Integer truncation in r_strdup leads to heap OOB read in Socket process via PMediaTransport::CreateIceCtx
Categories
(Core :: WebRTC: Networking, defect, P2)
Tracking
()
People
(Reporter: bugmon, Assigned: bwc)
Details
(6 keywords, Whiteboard: [adv-main150+r][adv-esr140.10.1+r])
Attachments
(7 files, 3 external files)
r_strdup() in nrappkit uses int len = strlen(str)+1 to compute the allocation size. When the input string is exactly 0xFFFFFFFF bytes long, strlen+1 = 0x100000000 (a 64-bit size_t) is truncated to 0 when stored in the signed 32-bit int. malloc(0) succeeds and returns a minimal stub allocation, memcpy copies zero bytes, and the function returns a heap pointer with no NUL terminator and uninitialized contents.
A compromised content process can trigger this in the Socket process via PMediaTransport::CreateIceCtx, which accepts an unbounded std::string. ParamTraitsStd<std::basic_string> deserializes a uint32_t length with no upper bound, and strings above 64KB are transferred via shared memory rather than the 256MB-capped pipe. The Socket process passes the received string's c_str() directly to nr_ice_ctx_create -> r_strdup, storing the broken pointer as ctx->label. This label is then formatted via r_log("ICE(%s)...", ctx->label), causing strnlen to read past the malloc(0) allocation into adjacent heap memory.
The leaked heap bytes are written into the RLogConnector ring buffer, which the content process can read back via the PMediaTransport::GetIceLog IPC -- completing a Socket-process heap information-disclosure primitive useful for ASLR bypass in a sandbox-escape chain. The corrupted ctx->label is also used in dozens of LOG_INFO/LOG_WARNING/LOG_ERR sites that format unconditionally (RLogConnector::ShouldLog returns true for all levels <= LOG_INFO), so no logging pref is required to hit the OOB once ICE operations proceed.
Build Info
- Branch: main
- Revision: 6164ea4bacaeaed1f617c11911df7fc32f2e6ec2
- Timestamp: 2026-04-02T19:36:24+00:00
Affected Code
File: dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_memory.cpp, line 122-138
char *r_strdup(const char *str)
{
int len; // signed 32-bit
char *nstr = 0;
if(!str)
return(0);
len=strlen(str)+1; // 0xFFFFFFFF + 1 = 0x100000000 (size_t) -> (int)0
if(!(nstr=(char*)RMALLOC(len))) // RMALLOC = malloc; malloc(0) -> 1-byte stub
return(0);
memcpy(nstr,str,len); // memcpy(_,_, 0) -> nothing copied, no NUL
return(nstr); // returns non-terminated, uninitialized stub
}
File: dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_ctx.cpp, line 317-325
if(!(ctx->label=r_strdup(label))) // <- broken pointer stored
ABORT(R_NO_MEMORY);
ctx->gather_handler = gather_handler;
/* Get the STUN servers */
if(r=NR_reg_get_child_count(NR_ICE_REG_STUN_SRV_PRFX,
(unsigned int *)&ctx->stun_server_ct_cfg)||ctx->stun_server_ct_cfg==0) {
r_log(LOG_ICE,LOG_DEBUG,"ICE(%s): No STUN servers specified in nICEr registry", ctx->label); // <- OOB read
File: ipc/chromium/src/chrome/common/ipc_message_utils.h, line 884-895
template <class T>
struct ParamTraitsStd<std::basic_string<T>> {
typedef std::basic_string<T> param_type;
static void Write(MessageWriter* writer, const param_type& p) {
WriteSequenceParam<const T&>(writer, p.data(), p.size());
}
static bool Read(MessageReader* reader, param_type* r) {
return ReadSequenceParam<T>(reader, [&](uint32_t length) -> T* {
r->resize(length); // no upper bound; accepts 0xFFFFFFFF
return r->data();
});
}
};
File: ipc/chromium/src/chrome/common/ipc_message_utils.h, line 692-701
template <typename P, typename F>
[[nodiscard]] bool ReadSequenceParam(MessageReader* reader, F&& allocator) {
uint32_t length = 0;
if (!reader->ReadUInt32(&length)) { // max 0xFFFFFFFF, no cap
reader->FatalError("failed to read byte length in ReadSequenceParam");
return false;
}
return ReadSequenceParamImpl<P>(reader, allocator(length), length);
}
File: ipc/chromium/src/chrome/common/ipc_message_utils.cc, line 61-93
MessageBufferReader::MessageBufferReader(MessageReader* reader,
uint32_t full_len)
: reader_(reader) {
// NOTE: We only write out the `shmem_ok` bool if we're over
// kMessageBufferShmemThreshold to avoid bloating the size of messages with
// small buffers.
if (full_len > kMessageBufferShmemThreshold) { // 64KB -> shmem path
bool shmem_ok = false;
if (!reader->ReadBool(&shmem_ok)) { ... }
if (shmem_ok) {
mozilla::ipc::shared_memory::MutableHandle handle;
if (!IPC::ReadParam(reader, &handle)) { ... }
... // bypasses 256MB pipe limit
shmem_cursor_ = mozilla::MakeUnique<...>(std::move(handle));
}
}
remaining_ = full_len;
}
File: dom/media/webrtc/transport/nricectx.cpp, line 695-696
r = nr_ice_ctx_create(const_cast<char*>(name_.c_str()), flags,
ice_gather_handler_, &ctx_); // strlen(name_.c_str()) == 0xFFFFFFFF
File: dom/media/webrtc/transport/rlogconnector.cpp, line 74-77
bool RLogConnector::ShouldLog(int level) const {
return level <= LOG_INFO || // LOG_INFO=6: everything except LOG_DEBUG
MOZ_LOG_TEST(getLogModule(), rLogLvlToMozLogLvl(level)); // formats unconditionally
}
The root cause is the use of int for a length derived from strlen(), which returns size_t. On 64-bit platforms, a string of length 0xFFFFFFFF causes strlen+1 to equal 0x100000000, which truncates to 0 when assigned to a 32-bit int. This is the smallest input that produces zero -- smaller overflows yield negative int values which cause malloc((size_t)negative) to fail and return NULL, which is handled safely. The 0xFFFFFFFF case uniquely succeeds with an empty allocation. The IPC layer permits this exact length: the wire format uses uint32_t (max 0xFFFFFFFF), shmem transport bypasses pipe size limits, and there is no receive-side maximum-length check anywhere in the deserialization path or in MediaTransportParent::RecvCreateIceCtx.
Exploit Chain
- Attacker compromises a content process (e.g., via a separate renderer vulnerability).
- Compromised content allocates a std::string of length 0xFFFFFFFF (4GB minus one byte) filled with non-NUL bytes.
- Content sends PMediaTransport::CreateIceCtx with this string. WriteSequenceParam writes a uint32_t length of 0xFFFFFFFF; MessageBufferWriter routes the 4GB payload through a shared-memory region (kMessageBufferShmemThreshold = 64KB).
- Socket process: PMediaTransportParent::OnMessageReceived deserializes via ReadSequenceParam -> r->resize(0xFFFFFFFF) -> reads 4GB from shmem. No bounds check.
- MediaTransportParent::RecvCreateIceCtx passes the string by const ref to MediaTransportHandlerSTS::CreateIceCtx, which (after two [=] lambda hops on a high-RAM system, peak ~8GB) calls NrIceCtx::Create(aName).
- NrIceCtx constructor copies the string into name_ member; Initialize() calls nr_ice_ctx_create(name_.c_str(), ...). strlen() returns 0xFFFFFFFF.
- r_strdup: int len = 0xFFFFFFFF + 1 -> truncated to 0 -> malloc(0) returns 1-byte stub -> memcpy 0 bytes -> ctx->label is a non-NUL-terminated heap pointer with uninitialized contents.
- nr_ice_ctx_create line 325 (or any of 30+ subsequent LOG_INFO/LOG_WARNING sites): r_log("ICE(%s)...", ctx->label) -> ringbuffer_vlog -> VsprintfLiteral -> strnlen reads past the 1-byte allocation into adjacent heap.
- The OOB bytes are formatted into a log line and stored in the RLogConnector ring buffer.
- Content sends PMediaTransport::GetIceLog -> RecvGetIceLog returns the ring buffer contents -> leaked Socket-process heap bytes are delivered back to the attacker, providing an info-leak primitive for ASLR bypass.
IPC Path
-
PMediaTransport CreateIceCtx
- Protocol: PMediaTransport
- Parent process: Socket
- Child process: Content
- Message flow: Content -> Socket: CreateIceCtx(string name) [4GB via shmem]
-
PMediaTransport GetIceLog readback
- Protocol: PMediaTransport
- Parent process: Socket
- Child process: Content
- Message flow: Content -> Socket: GetIceLog() returns (WebrtcGlobalLog) [leaked heap bytes]
Steps to Reproduce
- Apply the content-process patch to dom/media/webrtc/jsapi/MediaTransportHandlerIPC.cpp (gated by XRE_IsContentProcess(), sends a 0xFFFFFFFF-byte std::string via SendCreateIceCtx when the page URL contains 'r_strdup_int_trunc').
- Apply the socket-process memory-optimization patch to dom/media/webrtc/jsapi/MediaTransportHandler.cpp (gated by XRE_IsSocketProcess() && aName.length() == 0xFFFFFFFFULL, calls NrIceCtx::Create(aName) by const-ref instead of through two [=] by-value lambda captures, to fit within ASAN's hard_rss_limit_mb=12288). This patch only avoids the ~8GB peak of the unmodified lambda chain -- it does not change the call target, the input string, or any validation logic.
- Build Firefox with ASAN.
- Set prefs: media.peerconnection.mtransport_process=true, network.process.enabled=true (both default-on in production -- enables the Socket-process IPC path), logging.nicer=5 (enables LOG_DEBUG so the OOB is observable at the earliest site, ice_ctx.cpp:325; without this pref the corrupted ctx->label still crashes on later LOG_INFO/LOG_WARNING/LOG_ERR sites which RLogConnector::ShouldLog formats unconditionally).
- Load a page named r_strdup_int_trunc.html that creates an RTCPeerConnection.
- Observe ASAN heap-buffer-overflow READ in the Socket process at strnlen, with the 1-byte allocation traced to r_strdup at r_memory.cpp:132.
Security Impact
- Severity: Moderate
- Attacker capability: A compromised content process can leak Socket-process heap memory contents. The OOB read in strnlen captures bytes adjacent to a malloc(0) allocation; these bytes are formatted into a log line, stored in the RLogConnector ring buffer, and can be retrieved by content via the PMediaTransport::GetIceLog IPC. This provides an information-disclosure primitive (heap pointers, secrets, ASLR slide) in the Socket process, useful as a building block in a content-to-socket sandbox-escape chain. The corrupted ctx->label persists for the lifetime of the ICE context and is dereferenced at 30+ r_log call sites during normal ICE operation.
- Preconditions: Requires prior content-process compromise (renderer RCE). The Socket process must have ~8GB available RAM to survive the unmodified [=] lambda by-value captures of the 4GB string before reaching r_strdup -- feasible in release builds on 16GB+ desktop systems, but a significant constraint that limits target population. The 0xFFFFFFFF length is the unique value that produces malloc(0); smaller overflows yield negative int values which fail safely in malloc.
ASAN Report
=================================================================
==18372==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x788bb10ef191 at pc 0x595152176aef bp 0x786b6feedf50 sp 0x786b6feed718
READ of size 2 at 0x788bb10ef191 thread T4
#0 0x595152176aee in ___interceptor_strnlen /builds/worker/fetches/llvm-project/compiler-rt/lib/asan/../sanitizer_common/sanitizer_common_interceptors.inc:439:5
#1 0x5951522f635b in mozilla::PrintfTarget::cvt_s(char const*, int, int, int) /firefox/mozglue/misc/Printf.cpp:387:18
#2 0x5951522f9019 in mozilla::PrintfTarget::vprint(char const*, __va_list_tag*) /firefox/mozglue/misc/Printf.cpp:1010:16
#3 0x786b8764f4b1 in VsprintfBuf(char*, unsigned long, char const*, __va_list_tag*) /firefox/obj-firefox-asan/dist/include/mozilla/Sprintf.h:62:6
#4 0x786b8764f4b1 in int VsprintfLiteral<4096ul>(char (&) [4096ul], char const*, __va_list_tag*) /firefox/obj-firefox-asan/dist/include/mozilla/Sprintf.h:82:10
#5 0x786b8764f4b1 in ringbuffer_vlog(int, int, char const*, __va_list_tag*) /firefox/dom/media/webrtc/transport/rlogconnector.cpp:31:5
#6 0x786b94ae18b9 in r_vlog(int, int, char const*, __va_list_tag*) /firefox/dom/media/webrtc/transport/third_party/nrappkit/src/log/r_log.cpp:358:9
#7 0x786b94ae1503 in r_log(int, int, char const*, ...) /firefox/dom/media/webrtc/transport/third_party/nrappkit/src/log/r_log.cpp:291:5
#8 0x786b94a94f91 in nr_ice_ctx_create(char*, unsigned int, nr_ice_gather_handler_*, nr_ice_ctx_**) /firefox/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_ctx.cpp:325:7
#9 0x786b876178e8 in mozilla::NrIceCtx::Initialize() /firefox/dom/media/webrtc/transport/nricectx.cpp:695:7
#10 0x786b876173ee in mozilla::NrIceCtx::Create(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&) /firefox/dom/media/webrtc/transport/nricectx.cpp:274:13
#11 0x786b8f5f1051 in mozilla::MediaTransportHandlerSTS::CreateIceCtx(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&) /firefox/dom/media/webrtc/jsapi/MediaTransportHandler.cpp:553:28
#12 0x786b8f60dd6d in mozilla::MediaTransportParent::RecvCreateIceCtx(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&) /firefox/dom/media/webrtc/jsapi/MediaTransportParent.cpp:171:20
#13 0x786b8f5760d1 in mozilla::dom::PMediaTransportParent::OnMessageReceived(IPC::Message const&) /firefox/obj-firefox-asan/ipc/ipdl/PMediaTransportParent.cpp:515:88
#14 0x786b8714d7f5 in mozilla::ipc::MessageChannel::DispatchAsyncMessage(mozilla::ipc::ActorLifecycleProxy*, IPC::Message const&) /firefox/ipc/glue/MessageChannel.cpp:1798:25
#15 0x786b87149bce in mozilla::ipc::MessageChannel::DispatchMessage(mozilla::ipc::ActorLifecycleProxy*, std::unique_ptr<IPC::Message, std::default_delete<IPC::Message>>) /firefox/ipc/glue/MessageChannel.cpp:1724:9
#16 0x786b8714ab87 in mozilla::ipc::MessageChannel::RunMessage(mozilla::ipc::ActorLifecycleProxy*, mozilla::ipc::MessageChannel::MessageTask&) /firefox/ipc/glue/MessageChannel.cpp:1513:3
#17 0x786b8714c02e in mozilla::ipc::MessageChannel::MessageTask::Run() /firefox/ipc/glue/MessageChannel.cpp:1615:14
#18 0x786b856c93d7 in mozilla::TaskQueue::Runner::Run() /firefox/xpcom/threads/TaskQueue.cpp:309:20
#19 0x786b856f0ca7 in nsThreadPool::Run() /firefox/xpcom/threads/nsThreadPool.cpp:444:14
#20 0x786b856e3e9a in nsThread::ProcessNextEvent(bool, bool*) /firefox/xpcom/threads/nsThread.cpp:1173:16
#21 0x786b856ec869 in NS_ProcessNextEvent(nsIThread*, bool) /firefox/xpcom/threads/nsThreadUtils.cpp:465:10
#22 0x786b87159491 in mozilla::ipc::MessagePumpForNonMainThreads::Run(base::MessagePump::Delegate*) /firefox/ipc/glue/MessagePump.cpp:297:20
#23 0x786b86f9b1e4 in MessageLoop::Run() /firefox/ipc/chromium/src/base/message_loop.cc:346:3
0x788bb10ef191 is located 0 bytes after 1-byte region [0x788bb10ef190,0x788bb10ef191)
allocated by thread T4 here:
#0 0x5951521fea04 in __interceptor_malloc _asan_rtl_:3
#1 0x786b94ae0d08 in r_strdup(char const*) /firefox/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_memory.cpp:132:22
#2 0x786b94a94eeb in nr_ice_ctx_create(char*, unsigned int, nr_ice_gather_handler_*, nr_ice_ctx_**) /firefox/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_ctx.cpp:317:21
#3 0x786b876178e8 in mozilla::NrIceCtx::Initialize() /firefox/dom/media/webrtc/transport/nricectx.cpp:695:7
#4 0x786b876173ee in mozilla::NrIceCtx::Create(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&) /firefox/dom/media/webrtc/transport/nricectx.cpp:274:13
#5 0x786b8f5f1051 in mozilla::MediaTransportHandlerSTS::CreateIceCtx(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&) /firefox/dom/media/webrtc/jsapi/MediaTransportHandler.cpp:553:28
#6 0x786b8f60dd6d in mozilla::MediaTransportParent::RecvCreateIceCtx(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&) /firefox/dom/media/webrtc/jsapi/MediaTransportParent.cpp:171:20
#7 0x786b8f5760d1 in mozilla::dom::PMediaTransportParent::OnMessageReceived(IPC::Message const&) /firefox/obj-firefox-asan/ipc/ipdl/PMediaTransportParent.cpp:515:88
Thread T4 created by T0 (Socket Process) here:
#15 0x786b86c25f17 in mozilla::net::SocketProcessImpl::Init(int, char**) /firefox/netwerk/ipc/SocketProcessImpl.cpp:63:31
#16 0x786b9489c19a in XRE_InitChildProcess(int, char**, XREChildData const*) /firefox/toolkit/xre/nsEmbedFunctions.cpp:561:21
SUMMARY: AddressSanitizer: heap-buffer-overflow
Shadow bytes around the buggy address:
=>0x788bb10ef180: fa fa[01]fa fa fa fa fa fa fa fa fa fa fa fa fa
==18372==ABORTING
Updated•7 months ago
|
| Assignee | ||
Updated•7 months ago
|
| Reporter | ||
Comment 1•7 months ago
|
||
| Reporter | ||
Comment 2•7 months ago
|
||
| Reporter | ||
Comment 3•7 months ago
|
||
| Reporter | ||
Comment 4•7 months ago
|
||
| Reporter | ||
Comment 5•7 months ago
|
||
| Reporter | ||
Comment 6•7 months ago
|
||
| Reporter | ||
Comment 7•7 months ago
|
||
| Assignee | ||
Updated•7 months ago
|
| Assignee | ||
Comment 9•7 months ago
•
|
||
We should be limiting this much tighter than INT_MAX, maybe 1KB (for the context name).
| Assignee | ||
Comment 10•7 months ago
|
||
I think the most sensible thing is to use the C standard strdup, and remove r_strdup. Make sense to you Michael?
Comment 11•7 months ago
|
||
(In reply to Byron Campen [:bwc] from comment #10)
I think the most sensible thing is to use the C standard strdup, and remove r_strdup. Make sense to you Michael?
All the callers of r_strdup assume they'll release the RMALLOC'd memory with RFREE. Switching to C's impl of strdup might break something. I'd need to look more to be certain.
| Assignee | ||
Comment 12•7 months ago
|
||
Hmm, yeah, we'd need to knock out r_malloc and r_free as well to do this (probably a good thing), or fix all the places where these strings are freed (difficult).
| Assignee | ||
Comment 13•7 months ago
|
||
Ok, I am going to rip out all of the custom memory management in nICEr. That will stomp r_strdup as a side-effect, fixing this bug, and perhaps fixing some other not-yet-known problems.
| Assignee | ||
Comment 14•7 months ago
|
||
There is no reason to have something like this nowadays.
Comment 15•7 months ago
|
||
Comment 16•7 months ago
|
||
Comment 17•7 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: Sec bug
- Code covered by automated testing?: no
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: medium
- Explanation of risk level: We're ripping out a bunch of custom memory management code in our ICE stack, and using standard stuff instead. It is possible this will have some sort of side-effect.
- String changes made/needed?: None
- Is Android affected?: yes
| Assignee | ||
Comment 18•7 months ago
|
||
There is no reason to have something like this nowadays.
Original Revision: https://phabricator.services.mozilla.com/D293164
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Comment 19•7 months ago
|
||
| uplift | ||
Updated•7 months ago
|
| Assignee | ||
Comment 20•7 months ago
|
||
I had been considering uplifts on this, but did not have time last week to get patches ready. If you were wontfixing on my behalf, that was not my intent. What's your take?
Comment 21•7 months ago
•
|
||
I was looking at is as a medium-risk sec-moderate landing late in the cycle. If you think we should take it on ESR140 still, feel free to set the status back to affected and request uplift. I would say it's out of scope for ESR115 without a compelling argument for taking it, however.
| Assignee | ||
Comment 22•7 months ago
|
||
I could probably put together a less expansive patch for ESR uplift I suppose.
| Assignee | ||
Comment 23•6 months ago
|
||
There is no reason to have something like this nowadays.
Updated•6 months ago
|
Updated•6 months ago
|
Updated•6 months ago
|
Updated•6 months ago
|
Updated•6 months ago
|
Comment 24•6 months ago
|
||
| uplift | ||
Updated•6 months ago
|
Comment 25•6 months ago
|
||
| 140.10.1 uplift | ||
Updated•6 months ago
|
Updated•5 months ago
|
Updated•2 months ago
|
Description
•