CVE-2025-1016
Overview
Background
- `SourceSurfaceSharedDataWrapper`
- the GPU/compositor-process wrapper around a shared-memory-backed image surface that other processes hand off for rendering.
- `mConsumers`
- a reference count on
SourceSurfaceSharedDataWrapperrecording how many external-image references currently hold the surface alive. - Expiration tracking
SharedSurfacesParentkeeps unmapped-but-live surfaces in annsExpirationTracker(viaAddTracking/RemoveTracking) so idle surfaces can be aged out and freed.- `mMapCount`
- a per-wrapper count of active
Mapcalls that keeps the shared memory mapped while any consumer is reading it.
Root Cause Analysis
Both SharedSurfacesParent::Add and SharedSurfacesParent::AddSameProcess inserted a surface into mSurfaces and then called surface->AddConsumer(), but the surface object was constructed with mConsumers initialized to 0, creating a window in which a wrapper could legitimately observe a consumer count of 0. The mapping paths did not respect that count: SourceSurfaceSharedDataWrapper::Map unconditionally called SharedSurfacesParent::RemoveTracking(this) and SourceSurfaceSharedDataWrapper::Unmap unconditionally called SharedSurfacesParent::AddTracking(this) when mMapCount reached 0, so a surface with zero consumers could be inserted into or removed from the expiration tracker. Because the expiration tracker frees surfaces it owns, mutating tracking membership for a surface that no consumer is holding lets the expiration machinery act on an object whose lifetime is no longer guaranteed, producing a use-after-free / memory-corruption condition.
The fix makes the invariant that a tracked surface always has at least one consumer explicit: mConsumers now defaults to 1 (so the wrapper begins life owned), the redundant AddConsumer() calls in Add/AddSameProcess are removed, and Map/Unmap gate the RemoveTracking/AddTracking calls on mConsumers > 0. Assertions in AddTrackingLocked and ExpireMap enforce GetConsumers() > 0 so any future violation is caught.
mConsumers to 1 and only touching tracking while mConsumers > 0.Attack Path
- Register a shared surface
A content/compositor client causes
SharedSurfacesParent::Addto wrap externally supplied shared memory into aSourceSurfaceSharedDataWrapperand register it as a WebRender external image. - Drive the consumer count to zero
By racing or repeating add/remove and map operations, the attacker reaches a state where
mConsumersis0while the surface is still reachable throughmSurfaces. - Toggle expiration tracking
Calls into
SourceSurfaceSharedDataWrapper::MapandUnmapinvokeRemoveTracking/AddTrackingon the zero-consumer surface, inserting it into thensExpirationTrackerwithout a live owner. - Trigger the free
The expiration generation ages out and frees the tracked surface via
AgeAndExpireOneGeneration/ExpireMapeven though a reference is still in use. - Reuse the dangling surface
A subsequent
Mapor render access dereferences the freedSourceSurfaceSharedDataWrapper/backing buffer, giving a use-after-free that can be groomed for arbitrary code execution.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgfx/layers/SourceSurfaceSharedData.cpp |
modified | |
mCreatorRefgfx/layers/SourceSurfaceSharedData.h |
modified | |
forgfx/layers/ipc/SharedSurfacesParent.cpp |
modified |
Files Changed
gfx/layers/SourceSurfaceSharedData.cppgfx/layers/SourceSurfaceSharedData.hgfx/layers/ipc/SharedSurfacesParent.cpp
Audit Directions
- Refcount/lifetime couplingAudit every place a lifetime-tracking structure (expiration trackers, caches, LRU lists) is mutated to confirm membership is gated on the object’s own reference count rather than on unrelated state like
mMapCount. - Deferred initialization windowsLook for objects whose reference counts are set to
0at construction and incremented later, since any access between insertion and the increment can observe an unowned object; prefer initializing owned objects to1. - Assertion-only invariantsSearch for
MOZ_ASSERT-guarded invariants (like the oldGetConsumersassert) that vanish in release builds, and verify the invariant is also enforced by real runtime guards on the code paths that free memory.
Patch
diff --git a/gfx/layers/SourceSurfaceSharedData.cpp b/gfx/layers/SourceSurfaceSharedData.cpp
index b8d1456fba0..793b6bfa9c7 100644
--- a/gfx/layers/SourceSurfaceSharedData.cpp
+++ b/gfx/layers/SourceSurfaceSharedData.cpp
@@ -105,7 +105,9 @@ bool SourceSurfaceSharedDataWrapper::Map(MapType aMapType,
MutexAutoLock lock(*mHandleLock);
dataPtr = GetData();
if (mMapCount == 0) {
- SharedSurfacesParent::RemoveTracking(this);
+ if (mConsumers > 0) {
+ SharedSurfacesParent::RemoveTracking(this);
+ }
if (!dataPtr) {
size_t len = GetAlignedDataLength();
if (!EnsureMapped(len)) {
@@ -129,7 +131,7 @@ bool SourceSurfaceSharedDataWrapper::Map(MapType aMapType,
void SourceSurfaceSharedDataWrapper::Unmap() {
if (mHandleLock) {
MutexAutoLock lock(*mHandleLock);
- if (--mMapCount == 0) {
+ if (--mMapCount == 0 && mConsumers > 0) {
SharedSurfacesParent::AddTracking(this);
}
} else {
diff --git a/gfx/layers/SourceSurfaceSharedData.h b/gfx/layers/SourceSurfaceSharedData.h
index 1f92dbabd72..b17d6c4fed6 100644
--- a/gfx/layers/SourceSurfaceSharedData.h
+++ b/gfx/layers/SourceSurfaceSharedData.h
@@ -40,12 +40,7 @@ class SourceSurfaceSharedDataWrapper final : public DataSourceSurface {
MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(SourceSurfaceSharedDataWrapper,
override)
- SourceSurfaceSharedDataWrapper()
- : mStride(0),
- mConsumers(0),
- mFormat(SurfaceFormat::UNKNOWN),
- mCreatorPid(0),
- mCreatorRef(true) {}
+ SourceSurfaceSharedDataWrapper() = default;
void Init(const IntSize& aSize, int32_t aStride, SurfaceFormat aFormat,
SharedMemory::Handle aHandle, base::ProcessId aCreatorPid);
@@ -86,10 +81,7 @@ class SourceSurfaceSharedDataWrapper final : public DataSourceSurface {
return --mConsumers == 0;
}
- uint32_t GetConsumers() const {
- MOZ_ASSERT(mConsumers > 0);
- return mConsumers;
- }
+ uint32_t GetConsumers() const { return mConsumers; }
bool HasCreatorRef() const { return mCreatorRef; }
@@ -113,13 +105,13 @@ class SourceSurfaceSharedDataWrapper final : public DataSourceSurface {
// Protects mapping and unmapping of mBuf.
Maybe<Mutex> mHandleLock;
nsExpirationState mExpirationState;
- int32_t mStride;
- uint32_t mConsumers;
+ int32_t mStride = 0;
+ uint32_t mConsumers = 1;
IntSize mSize;
RefPtr<SharedMemory> mBuf;
- SurfaceFormat mFormat;
- base::ProcessId mCreatorPid;
- bool mCreatorRef;
+ SurfaceFormat mFormat = SurfaceFormat::UNKNOWN;
+ base::ProcessId mCreatorPid = 0;
+ bool mCreatorRef = true;
};
/**
diff --git a/gfx/layers/ipc/SharedSurfacesParent.cpp b/gfx/layers/ipc/SharedSurfacesParent.cpp
index b2c3946d065..0a6d851f4a8 100644
--- a/gfx/layers/ipc/SharedSurfacesParent.cpp
+++ b/gfx/layers/ipc/SharedSurfacesParent.cpp
@@ -200,7 +200,6 @@ void SharedSurfacesParent::AddSameProcess(const wr::ExternalImageId& aId,
auto texture = MakeRefPtr<wr::RenderSharedSurfaceTextureHost>(surface);
wr::RenderThread::Get()->RegisterExternalImage(aId, texture.forget());
- surface->AddConsumer();
sInstance->mSurfaces.InsertOrUpdate(id, std::move(surface));
}
@@ -269,7 +268,6 @@ void SharedSurfacesParent::Add(const wr::ExternalImageId& aId,
auto texture = MakeRefPtr<wr::RenderSharedSurfaceTextureHost>(surface);
wr::RenderThread::Get()->RegisterExternalImage(aId, texture.forget());
- surface->AddConsumer();
sInstance->mSurfaces.InsertOrUpdate(id, std::move(surface));
}
@@ -284,6 +282,7 @@ void SharedSurfacesParent::AddTrackingLocked(
SourceSurfaceSharedDataWrapper* aSurface,
const StaticMutexAutoLock& aAutoLock) {
MOZ_ASSERT(!aSurface->GetExpirationState()->IsTracked());
+ MOZ_ASSERT(aSurface->GetConsumers() > 0);
sInstance->mTracker.AddObjectLocked(aSurface, aAutoLock);
}
@@ -356,6 +355,7 @@ bool SharedSurfacesParent::AgeAndExpireOneGeneration() {
void SharedSurfacesParent::ExpireMap(
nsTArray<RefPtr<SourceSurfaceSharedDataWrapper>>& aExpired) {
for (auto& surface : aExpired) {
+ MOZ_ASSERT(surface->GetConsumers() > 0);
surface->ExpireMap();
}
}