// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "ppapi/proxy/ppb_image_data_proxy.h"
#include <string.h> // For memcpy
#include <map>
#include <vector>
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/shared_memory_mapping.h"
#include "base/memory/singleton.h"
#include "base/memory/weak_ptr.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/nacl/common/buildflags.h"
#include "ppapi/c/pp_completion_callback.h"
#include "ppapi/c/pp_errors.h"
#include "ppapi/c/pp_resource.h"
#include "ppapi/proxy/enter_proxy.h"
#include "ppapi/proxy/host_dispatcher.h"
#include "ppapi/proxy/plugin_dispatcher.h"
#include "ppapi/proxy/plugin_globals.h"
#include "ppapi/proxy/plugin_resource_tracker.h"
#include "ppapi/proxy/ppapi_messages.h"
#include "ppapi/shared_impl/host_resource.h"
#include "ppapi/shared_impl/proxy_lock.h"
#include "ppapi/shared_impl/resource.h"
#include "ppapi/shared_impl/scoped_pp_resource.h"
#include "ppapi/thunk/enter.h"
#include "ppapi/thunk/thunk.h"
#if !BUILDFLAG(IS_NACL) && !BUILDFLAG(IS_MINIMAL_TOOLCHAIN)
#include "skia/ext/platform_canvas.h" //nogncheck
#include "ui/surface/transport_dib.h" //nogncheck
#endif
using ppapi::thunk::PPB_ImageData_API;
namespace ppapi {
namespace proxy {
namespace {
// How ImageData re-use works
// --------------------------
//
// When animating plugins (like video), re-creating image datas for each frame
// and mapping the memory has a high overhead. So we try to re-use these when
// possible.
//
// 1. Plugin makes an asynchronous call that transfers an ImageData to the
// implementation of some API.
// 2. Plugin frees its ImageData reference. If it doesn't do this we can't
// re-use it.
// 3. When the last plugin ref of an ImageData is released, we don't actually
// delete it. Instead we put it on a queue where we hold onto it in the
// plugin process for a short period of time.
// 4. The API implementation that received the ImageData finishes using it.
// Without our caching system it would get deleted at this point.
// 5. The proxy in the renderer will send NotifyUnusedImageData back to the
// plugin process. We check if the given resource is in the queue and mark
// it as usable.
// 6. When the plugin requests a new image data, we check our queue and if there
// is a usable ImageData of the right size and format, we'll return it
// instead of making a new one. It's important that caching is only requested
// when the size is unlikely to change, so cache hits are high.
//
// Some notes:
//
// - We only re-use image data when the plugin and host are rapidly exchanging
// them and the size is likely to remain constant. It should be clear that
// the plugin is promising that it's done with the image.
//
// - Theoretically we could re-use them in other cases but the lifetime
// becomes more difficult to manage. The plugin could have used an ImageData
// in an arbitrary number of queued up PaintImageData calls which we would
// have to check.
//
// - If a flush takes a long time or there are many released image datas
// accumulating in our queue such that some are deleted, we will have
// released our reference by the time the renderer notifies us of an unused
// image data. In this case we just give up.
//
// - We maintain a per-instance cache. Some pages have many instances of
// Flash, for example, each of a different size. If they're all animating we
// want each to get its own image data re-use.
//
// - We generate new resource IDs when re-use happens to try to avoid weird
// problems if the plugin messes up its refcounting.
// Keep a cache entry for this many seconds before expiring it. We get an entry
// back from the renderer after an ImageData is swapped out, so it means the
// plugin has to be painting at least two frames for this time interval to
// get caching.
static const int kMaxAgeSeconds = 2;
// ImageDataCacheEntry ---------------------------------------------------------
struct ImageDataCacheEntry {
ImageDataCacheEntry() : usable(false) {}
explicit ImageDataCacheEntry(ImageData* i)
: added_time(base::TimeTicks::Now()), usable(false), image(i) {}
base::TimeTicks added_time;
// Set to true when the renderer tells us that it's OK to re-use this image.
bool usable;
scoped_refptr<ImageData> image;
};
// ImageDataInstanceCache ------------------------------------------------------
// Per-instance cache of image datas.
class ImageDataInstanceCache {
public:
ImageDataInstanceCache() : next_insertion_point_(0) {}
// These functions have the same spec as the ones in ImageDataCache.
scoped_refptr<ImageData> Get(PPB_ImageData_Shared::ImageDataType type,
int width, int height,
PP_ImageDataFormat format);
void Add(ImageData* image_data);
void ImageDataUsable(ImageData* image_data);
// Expires old entries. Returns true if there are still entries in the list,
// false if this instance cache is now empty.
bool ExpireEntries();
private:
void IncrementInsertionPoint();
// We'll store this many ImageDatas per instance.
static const size_t kCacheSize = 2;
ImageDataCacheEntry images_[kCacheSize];
// Index into cache where the next item will go.
size_t next_insertion_point_;
};
scoped_refptr<ImageData> ImageDataInstanceCache::Get(
PPB_ImageData_Shared::ImageDataType type,
int width, int height,
PP_ImageDataFormat format) {
// Just do a brute-force search since the cache is so small.
for (size_t i = 0; i < kCacheSize; i++) {
if (!images_[i].usable)
continue;
if (images_[i].image->type() != type)
continue;
const PP_ImageDataDesc& desc = images_[i].image->desc();
if (desc.format == format &&
desc.size.width == width && desc.size.height == height) {
scoped_refptr<ImageData> ret(images_[i].image);
images_[i] = ImageDataCacheEntry();
// Since we just removed an item, this entry is the best place to insert
// a subsequent one.
next_insertion_point_ = i;
return ret;
}
}
return scoped_refptr<ImageData>();
}
void ImageDataInstanceCache::Add(ImageData* image_data) {
images_[next_insertion_point_] = ImageDataCacheEntry(image_data);
IncrementInsertionPoint();
}
void ImageDataInstanceCache::ImageDataUsable(ImageData* image_data) {
for (size_t i = 0; i < kCacheSize; i++) {
if (images_[i].image.get() == image_data) {
images_[i].usable = true;
// This test is important. The renderer doesn't guarantee how many image
// datas it has or when it notifies us when one is usable. Its possible
// to get into situations where it's always telling us the old one is
// usable, and then the older one immediately gets expired. Therefore,
// if the next insertion would overwrite this now-usable entry, make the
// next insertion overwrite some other entry to avoid the replacement.
if (next_insertion_point_ == i)
IncrementInsertionPoint();
return;
}
}
}
bool ImageDataInstanceCache::ExpireEntries() {
base::TimeTicks threshold_time =
base::TimeTicks::Now() - base::Seconds(kMaxAgeSeconds);
bool has_entry = false;
for (size_t i = 0; i < kCacheSize; i++) {
if (images_[i].image.get()) {
// Entry present.
if (images_[i].added_time <= threshold_time) {
// Found an entry to expire.
images_[i] = ImageDataCacheEntry();
next_insertion_point_ = i;
} else {
// Found an entry that we're keeping.
has_entry = true;
}
}
}
return has_entry;
}
void ImageDataInstanceCache::IncrementInsertionPoint() {
// Go to the next location, wrapping around to get LRU.
next_insertion_point_++;
if (next_insertion_point_ >= kCacheSize)
next_insertion_point_ = 0;
}
// ImageDataCache --------------------------------------------------------------
class ImageDataCache {
public:
ImageDataCache() {}
ImageDataCache(const ImageDataCache&) = delete;
ImageDataCache& operator=(const ImageDataCache&) = delete;
~ImageDataCache() {}
static ImageDataCache* GetInstance();
// Retrieves an image data from the cache of the specified type, size and
// format if one exists. If one doesn't exist, this will return a null refptr.
scoped_refptr<ImageData> Get(PP_Instance instance,
PPB_ImageData_Shared::ImageDataType type,
int width, int height,
PP_ImageDataFormat format);
// Adds the given image data to the cache. There should be no plugin
// references to it. This may delete an older item from the cache.
void Add(ImageData* image_data);
// Notification from the renderer that the given image data is usable.
void ImageDataUsable(ImageData* image_data);
void DidDeleteInstance(PP_Instance instance);
private:
friend struct base::LeakySingletonTraits<ImageDataCache>;
// Timer callback to expire entries for the given instance.
void OnTimer(PP_Instance instance);
typedef std::map<PP_Instance, ImageDataInstanceCache> CacheMap;
CacheMap cache_;
// This class does timer calls and we don't want to run these outside of the
// scope of the object. Technically, since this class is a leaked static,
// this will never happen and this factory is unnecessary. However, it's
// probably better not to make assumptions about the lifetime of this class.
base::WeakPtrFactory<ImageDataCache> weak_factory_{this};
};
// static
ImageDataCache* ImageDataCache::GetInstance() {
return base::Singleton<ImageDataCache,
base::LeakySingletonTraits<ImageDataCache>>::get();
}
scoped_refptr<ImageData> ImageDataCache::Get(
PP_Instance instance,
PPB_ImageData_Shared::ImageDataType type,
int width, int height,
PP_ImageDataFormat format) {
CacheMap::iterator found = cache_.find(instance);
if (found == cache_.end())
return scoped_refptr<ImageData>();
return found->second.Get(type, width, height, format);
}
void ImageDataCache::Add(ImageData* image_data) {
cache_[image_data->pp_instance()].Add(image_data);
// Schedule a timer to invalidate this entry.
PpapiGlobals::Get()->GetMainThreadMessageLoop()->PostDelayedTask(
FROM_HERE,
RunWhileLocked(base::BindOnce(&ImageDataCache::OnTimer,
weak_factory_.GetWeakPtr(),
image_data->pp_instance())),
base::Seconds(kMaxAgeSeconds));
}
void ImageDataCache::ImageDataUsable(ImageData* image_data) {
CacheMap::iterator found = cache_.find(image_data->pp_instance());
if (found != cache_.end())
found->second.ImageDataUsable(image_data);
}
void ImageDataCache::DidDeleteInstance(PP_Instance instance) {
cache_.erase(instance);
}
void ImageDataCache::OnTimer(PP_Instance instance) {
CacheMap::iterator found = cache_.find(instance);
if (found == cache_.end())
return;
if (!found->second.ExpireEntries()) {
// There are no more entries for this instance, remove it from the cache.
cache_.erase(found);
}
}
} // namespace
// ImageData -------------------------------------------------------------------
ImageData::ImageData(const HostResource& resource,
PPB_ImageData_Shared::ImageDataType type,
const PP_ImageDataDesc& desc)
: Resource(OBJECT_IS_PROXY, resource),
type_(type),
desc_(desc),
is_candidate_for_reuse_(false) {
}
ImageData::~ImageData() {
}
PPB_ImageData_API* ImageData::AsPPB_ImageData_API() {
return this;
}
void ImageData::LastPluginRefWasDeleted() {
// The plugin no longer needs this ImageData, add it to our cache if it's
// been used in a ReplaceContents. These are the ImageDatas that the renderer
// will send back ImageDataUsable messages for.
if (is_candidate_for_reuse_)
ImageDataCache::GetInstance()->Add(this);
}
void ImageData::InstanceWasDeleted() {
ImageDataCache::GetInstance()->DidDeleteInstance(pp_instance());
}
PP_Bool ImageData::Describe(PP_ImageDataDesc* desc) {
memcpy(desc, &desc_, sizeof(PP_ImageDataDesc));
return PP_TRUE;
}
int32_t ImageData::GetSharedMemoryRegion(
base::UnsafeSharedMemoryRegion** /* region */) {
// Not supported in the proxy (this method is for actually implementing the
// proxy in the host).
return PP_ERROR_NOACCESS;
}
void ImageData::SetIsCandidateForReuse() {
is_candidate_for_reuse_ = true;
}
void ImageData::RecycleToPlugin(bool zero_contents) {
is_candidate_for_reuse_ = false;
if (zero_contents) {
void* data = Map();
memset(data, 0, desc_.stride * desc_.size.height);
Unmap();
}
}
// PlatformImageData -----------------------------------------------------------
#if !BUILDFLAG(IS_NACL) && !BUILDFLAG(IS_MINIMAL_TOOLCHAIN)
PlatformImageData::PlatformImageData(
const HostResource& resource,
const PP_ImageDataDesc& desc,
base::UnsafeSharedMemoryRegion image_region)
: ImageData(resource, PPB_ImageData_Shared::PLATFORM, desc) {
#if BUILDFLAG(IS_WIN)
transport_dib_ = TransportDIB::CreateWithHandle(std::move(image_region));
#else
transport_dib_ = TransportDIB::Map(std::move(image_region));
#endif // BUILDFLAG(IS_WIN)
}
PlatformImageData::~PlatformImageData() = default;
void* PlatformImageData::Map() {
if (!mapped_canvas_.get()) {
if (!transport_dib_.get())
return nullptr;
const bool is_opaque = false;
mapped_canvas_ = transport_dib_->GetPlatformCanvas(
desc_.size.width, desc_.size.height, is_opaque);
if (!mapped_canvas_.get())
return nullptr;
}
SkPixmap pixmap;
skia::GetWritablePixels(mapped_canvas_.get(), &pixmap);
return pixmap.writable_addr();
}
void PlatformImageData::Unmap() {
// TODO(brettw) have a way to unmap a TransportDIB. Currently this isn't
// possible since deleting the TransportDIB also frees all the handles.
// We need to add a method to TransportDIB to release the handles.
}
SkCanvas* PlatformImageData::GetCanvas() {
return mapped_canvas_.get();
}
#endif // !BUILDFLAG(IS_NACL) && !BUILDFLAG(IS_MINIMAL_TOOLCHAIN)
// SimpleImageData -------------------------------------------------------------
SimpleImageData::SimpleImageData(const HostResource& resource,
const PP_ImageDataDesc& desc,
base::UnsafeSharedMemoryRegion region)
: ImageData(resource, PPB_ImageData_Shared::SIMPLE, desc),
shm_region_(std::move(region)),
size_(desc.size.width * desc.size.height * 4),
map_count_(0) {}
SimpleImageData::~SimpleImageData() = default;
void* SimpleImageData::Map() {
if (map_count_++ == 0)
shm_mapping_ = shm_region_.MapAt(0, size_);
return shm_mapping_.IsValid() ? shm_mapping_.memory() : nullptr;
}
void SimpleImageData::Unmap() {
if (--map_count_ == 0)
shm_mapping_ = base::WritableSharedMemoryMapping();
}
SkCanvas* SimpleImageData::GetCanvas() {
return nullptr; // No canvas available.
}
// PPB_ImageData_Proxy ---------------------------------------------------------
PPB_ImageData_Proxy::PPB_ImageData_Proxy(Dispatcher* dispatcher)
: InterfaceProxy(dispatcher) {
}
PPB_ImageData_Proxy::~PPB_ImageData_Proxy() {
}
// static
PP_Resource PPB_ImageData_Proxy::CreateProxyResource(
PP_Instance instance,
PPB_ImageData_Shared::ImageDataType type,
PP_ImageDataFormat format,
const PP_Size& size,
PP_Bool init_to_zero) {
PluginDispatcher* dispatcher = PluginDispatcher::GetForInstance(instance);
if (!dispatcher)
return 0;
// Check the cache.
scoped_refptr<ImageData> cached_image_data =
ImageDataCache::GetInstance()->Get(instance, type,
size.width, size.height, format);
if (cached_image_data.get()) {
// We have one we can re-use rather than allocating a new one.
cached_image_data->RecycleToPlugin(PP_ToBool(init_to_zero));
return cached_image_data->GetReference();
}
HostResource result;
PP_ImageDataDesc desc;
switch (type) {
case PPB_ImageData_Shared::SIMPLE: {
ppapi::proxy::SerializedHandle image_handle;
dispatcher->Send(new PpapiHostMsg_PPBImageData_CreateSimple(
kApiID, instance, format, size, init_to_zero, &result, &desc,
&image_handle));
if (image_handle.is_shmem_region()) {
base::UnsafeSharedMemoryRegion image_region =
base::UnsafeSharedMemoryRegion::Deserialize(
image_handle.TakeSharedMemoryRegion());
if (!result.is_null()) {
return (new SimpleImageData(result, desc, std::move(image_region)))
->GetReference();
}
}
break;
}
case PPB_ImageData_Shared::PLATFORM: {
#if !BUILDFLAG(IS_NACL) && !BUILDFLAG(IS_MINIMAL_TOOLCHAIN)
ppapi::proxy::SerializedHandle image_handle;
dispatcher->Send(new PpapiHostMsg_PPBImageData_CreatePlatform(
kApiID, instance, format, size, init_to_zero, &result, &desc,
&image_handle));
if (image_handle.is_shmem_region()) {
base::UnsafeSharedMemoryRegion image_region =
base::UnsafeSharedMemoryRegion::Deserialize(
image_handle.TakeSharedMemoryRegion());
if (!result.is_null()) {
return (new PlatformImageData(result, desc, std::move(image_region)))
->GetReference();
}
}
break;
#else
// PlatformImageData shouldn't be created in untrusted code.
NOTREACHED();
#endif
}
}
return 0;
}
bool PPB_ImageData_Proxy::OnMessageReceived(const IPC::Message& msg) {
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(PPB_ImageData_Proxy, msg)
#if !BUILDFLAG(IS_NACL) && !BUILDFLAG(IS_MINIMAL_TOOLCHAIN)
IPC_MESSAGE_HANDLER(PpapiHostMsg_PPBImageData_CreatePlatform,
OnHostMsgCreatePlatform)
IPC_MESSAGE_HANDLER(PpapiHostMsg_PPBImageData_CreateSimple,
OnHostMsgCreateSimple)
#endif
IPC_MESSAGE_HANDLER(PpapiMsg_PPBImageData_NotifyUnusedImageData,
OnPluginMsgNotifyUnusedImageData)
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
return handled;
}
#if !BUILDFLAG(IS_NACL) && !BUILDFLAG(IS_MINIMAL_TOOLCHAIN)
// static
PP_Resource PPB_ImageData_Proxy::CreateImageData(
PP_Instance instance,
PPB_ImageData_Shared::ImageDataType type,
PP_ImageDataFormat format,
const PP_Size& size,
bool init_to_zero,
PP_ImageDataDesc* desc,
base::UnsafeSharedMemoryRegion* image_region) {
HostDispatcher* dispatcher = HostDispatcher::GetForInstance(instance);
if (!dispatcher)
return 0;
thunk::EnterResourceCreation enter(instance);
if (enter.failed())
return 0;
PP_Bool pp_init_to_zero = init_to_zero ? PP_TRUE : PP_FALSE;
PP_Resource pp_resource = 0;
switch (type) {
case PPB_ImageData_Shared::SIMPLE: {
pp_resource = enter.functions()->CreateImageDataSimple(
instance, format, &size, pp_init_to_zero);
break;
}
case PPB_ImageData_Shared::PLATFORM: {
pp_resource = enter.functions()->CreateImageData(
instance, format, &size, pp_init_to_zero);
break;
}
}
if (!pp_resource)
return 0;
ppapi::ScopedPPResource resource(ppapi::ScopedPPResource::PassRef(),
pp_resource);
thunk::EnterResourceNoLock<PPB_ImageData_API> enter_resource(resource.get(),
false);
if (enter_resource.object()->Describe(desc) != PP_TRUE) {
DVLOG(1) << "CreateImageData failed: could not Describe";
return 0;
}
base::UnsafeSharedMemoryRegion* local_shm;
if (enter_resource.object()->GetSharedMemoryRegion(&local_shm) != PP_OK) {
DVLOG(1) << "CreateImageData failed: could not GetSharedMemory";
return 0;
}
*image_region =
dispatcher->ShareUnsafeSharedMemoryRegionWithRemote(*local_shm);
return resource.Release();
}
void PPB_ImageData_Proxy::OnHostMsgCreatePlatform(
PP_Instance instance,
int32_t format,
const PP_Size& size,
PP_Bool init_to_zero,
HostResource* result,
PP_ImageDataDesc* desc,
ppapi::proxy::SerializedHandle* result_image_handle) {
// Clear |desc| so we don't send uninitialized memory to the plugin.
// https://crbug.com/391023.
*desc = PP_ImageDataDesc();
base::UnsafeSharedMemoryRegion image_region;
PP_Resource resource =
CreateImageData(instance, PPB_ImageData_Shared::PLATFORM,
static_cast<PP_ImageDataFormat>(format), size,
true /* init_to_zero */, desc, &image_region);
result->SetHostResource(instance, resource);
if (resource) {
result_image_handle->set_shmem_region(
base::UnsafeSharedMemoryRegion::TakeHandleForSerialization(
std::move(image_region)));
} else {
result_image_handle->set_null_shmem_region();
}
}
void PPB_ImageData_Proxy::OnHostMsgCreateSimple(
PP_Instance instance,
int32_t format,
const PP_Size& size,
PP_Bool init_to_zero,
HostResource* result,
PP_ImageDataDesc* desc,
ppapi::proxy::SerializedHandle* result_image_handle) {
// Clear |desc| so we don't send uninitialized memory to the plugin.
// https://crbug.com/391023.
*desc = PP_ImageDataDesc();
base::UnsafeSharedMemoryRegion image_region;
PP_Resource resource =
CreateImageData(instance, PPB_ImageData_Shared::SIMPLE,
static_cast<PP_ImageDataFormat>(format), size,
true /* init_to_zero */, desc, &image_region);
result->SetHostResource(instance, resource);
if (resource) {
result_image_handle->set_shmem_region(
base::UnsafeSharedMemoryRegion::TakeHandleForSerialization(
std::move(image_region)));
} else {
result_image_handle->set_null_shmem_region();
}
}
#endif // !BUILDFLAG(IS_NACL) && !BUILDFLAG(IS_MINIMAL_TOOLCHAIN)
void PPB_ImageData_Proxy::OnPluginMsgNotifyUnusedImageData(
const HostResource& old_image_data) {
PluginGlobals* plugin_globals = PluginGlobals::Get();
if (!plugin_globals) {
return; // This may happen if the plugin is maliciously sending this
// message to the renderer.
}
EnterPluginFromHostResource<PPB_ImageData_API> enter(old_image_data);
if (enter.succeeded()) {
ImageData* image_data = static_cast<ImageData*>(enter.object());
ImageDataCache::GetInstance()->ImageDataUsable(image_data);
}
// The renderer sent us a reference with the message. If the image data was
// still cached in our process, the proxy still holds a reference so we can
// remove the one the renderer just sent is. If the proxy no longer holds a
// reference, we released everything and we should also release the one the
// renderer just sent us.
dispatcher()->Send(new PpapiHostMsg_PPBCore_ReleaseResource(
API_ID_PPB_CORE, old_image_data));
}
} // namespace proxy
} // namespace ppapi