CVE-2026-117999
Overview
Background
- `PlatformPipeLink`
- a class in
xpcom/io/AsyncPlatformPipes.cppthat drives an asynchronous platform pipe for Firefox IPC, deriving (on applicable platforms) fromMessageLoopForIO::Watcherso it can be notified of I/O readiness on the IO thread. - `MessageLoopForIO::Watcher`
- a base that registers the object with the IO thread’s message loop; its registration/watch state is owned by and must be torn down on the IO thread.
- `mIOThread`
- the IO thread/event target this link operates on; the link’s I/O callbacks and watch (de)registration run there.
- `NS_INLINE_DECL_THREADSAFE_REFCOUNTING`
- a macro giving a class thread-safe (atomic) reference counting where the object is destroyed on whichever thread happens to drop the last reference.
- `NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DELETE_ON_EVENT_TARGET(cls, target)`
- the same, except the final
Releaseproxies destruction to a specified event target, guaranteeing the destructor always runs on that thread.
Root Cause Analysis
PlatformPipeLink was declared with plain NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PlatformPipeLink). With that macro, the object is deleted on whatever thread releases the last strong reference. But PlatformPipeLink is a MessageLoopForIO::Watcher whose watch registration and I/O state are bound to the IO thread (mIOThread): its destructor tears down IO-thread-owned state (unregistering the watcher, closing/releasing the file-handle watch). If the last reference is dropped on a thread other than mIOThread — which can happen because references are held across threads in the IPC machinery — the destructor runs off the IO thread concurrently with, or in a wrong-thread relationship to, the IO thread’s own access to that watch state. That is the race: destruction on an arbitrary thread versus IO-thread use of the watcher, which can corrupt or use-after-free the watch registration.
The patch replaces the refcounting macro with NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DELETE_ON_EVENT_TARGET(PlatformPipeLink, mIOThread.GetEventTarget()). Now, whichever thread drops the final reference, the actual destruction is dispatched to and executed on the IO thread’s event target, so the destructor’s teardown of the Watcher registration always runs on the same thread that owns and services it. This serializes destruction against the IO thread’s use of the object and removes the cross-thread race. The advisory’s class (“race condition in IPC”) matches the diff exactly: it is a destruction-thread/lifetime race fixed by pinning deletion to the IO thread.
mIOThread’s event target removes the race.Attack Path
- IPC in use
a feature using
AsyncPlatformPipesholdsPlatformPipeLinkreferences on more than one thread (the IPC/IO threads), as the IPC runtime does (inferred: standard async-pipe usage). - Cross-thread last release a non-IO thread drops the final reference (e.g. during connection teardown / error), triggering destruction off the IO thread.
- Concurrent IO-thread access the IO thread is simultaneously servicing or unwinding the same watcher’s registration.
- Race window the destructor mutates/frees the watcher state while the IO thread reads or uses it.
- Primitive a data race leading to use-after-free/corruption of the pipe link’s watch state (timing-dependent; exploitability inferred, consistent with the low severity rating).
Impact Assessment
Files Changed
xpcom/io/AsyncPlatformPipes.cpp
Audit Directions
- Watchers/IO objects with plain threadsafe refcountingaudit other
MessageLoopForIO::Watchersubclasses and IO-thread-bound objects usingNS_INLINE_DECL_THREADSAFE_REFCOUNTINGto confirm they, too, pin destruction to the owning event target. - Cross-thread last-release hazardsreview refcounted objects whose destructor touches thread-affine state (socket/pipe/file-handle watches, nsITimer, event-loop registrations) for off-thread destruction possibilities.
- `AsyncPlatformPipes` lifetimetrace all holders of
PlatformPipeLinkreferences across threads to verify no other member is torn down on the wrong thread even with deletion now on the IO thread.
Patch
diff --git a/xpcom/io/AsyncPlatformPipes.cpp b/xpcom/io/AsyncPlatformPipes.cpp
index 51b9447b355..ba235e6128d 100644
--- a/xpcom/io/AsyncPlatformPipes.cpp
+++ b/xpcom/io/AsyncPlatformPipes.cpp
@@ -33,7 +33,8 @@ class PlatformPipeLink
: public MessageLoopForIO::Watcher
#endif
{
- NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PlatformPipeLink)
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DELETE_ON_EVENT_TARGET(
+ PlatformPipeLink, mIOThread.GetEventTarget())
public:
PlatformPipeLink(UniqueFileHandle aHandle, uint32_t aBufferSize);