Closed Bug 2029461 (CVE-2026-7321) Opened 7 months ago Closed 7 months ago

Integer truncation in r_strdup leads to heap OOB read in Socket process via PMediaTransport::CreateIceCtx

Categories

(Core :: WebRTC: Networking, defect, P2)

defect

Tracking

()

RESOLVED FIXED
151 Branch
Tracking Status
firefox-esr115 --- wontfix
firefox-esr140 150.0.1+ fixed
firefox149 --- wontfix
firefox150 + fixed
firefox151 + fixed

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

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

  1. Attacker compromises a content process (e.g., via a separate renderer vulnerability).
  2. Compromised content allocates a std::string of length 0xFFFFFFFF (4GB minus one byte) filled with non-NUL bytes.
  3. 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).
  4. Socket process: PMediaTransportParent::OnMessageReceived deserializes via ReadSequenceParam -> r->resize(0xFFFFFFFF) -> reads 4GB from shmem. No bounds check.
  5. 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).
  6. NrIceCtx constructor copies the string into name_ member; Initialize() calls nr_ice_ctx_create(name_.c_str(), ...). strlen() returns 0xFFFFFFFF.
  7. 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.
  8. 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.
  9. The OOB bytes are formatted into a log line and stored in the RLogConnector ring buffer.
  10. 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

  1. 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').
  2. 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.
  3. Build Firefox with ASAN.
  4. 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).
  5. Load a page named r_strdup_int_trunc.html that creates an RTCPeerConnection.
  6. 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
Group: core-security → media-core-security
Assignee: nobody → docfaraday
Attached file prefs.json —
Attached patch fix.patch — — Splinter Review
Attached file crash_stack.txt —
Attached file README.txt —
Attached file prefs.js —
Severity: -- → S2
Priority: -- → P2

We should be limiting this much tighter than INT_MAX, maybe 1KB (for the context name).

I think the most sensible thing is to use the C standard strdup, and remove r_strdup. Make sense to you Michael?

Flags: needinfo?(mfroman)

(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.

Flags: needinfo?(mfroman)

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).

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.

Attached file (secure) —

There is no reason to have something like this nowadays.

Group: media-core-security → core-security-release
Status: UNCONFIRMED → RESOLVED
Closed: 7 months ago
Resolution: --- → FIXED
Target Milestone: --- → 151 Branch

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
Attachment #9568920 - Flags: approval-mozilla-beta?
Attached file (secure) —

There is no reason to have something like this nowadays.

Original Revision: https://phabricator.services.mozilla.com/D293164

Attachment #9568920 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c151/b150]

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?

Flags: needinfo?(ryanvm)

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.

Flags: needinfo?(ryanvm)

I could probably put together a less expansive patch for ESR uplift I suppose.

Regressions: 2031662
No longer regressions: 2031662
Attached file (secure) —

There is no reason to have something like this nowadays.

Attachment #9570295 - Flags: approval-mozilla-esr140?
Whiteboard: [adv-main150+r]
Attachment #9570295 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
Whiteboard: [adv-main150+r] → [adv-main150+r][adv-esr140.10.1+r]
Alias: CVE-2026-7321
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: