Free of uninitialized pointer in rlbox_wasm2c_sandbox.hpp, triggerable in at least Android builds
Categories
(Core :: Security: RLBox, defect)
Tracking
()
People
(Reporter: nataliehoogland0, Unassigned)
Details
(Keywords: reporter-external, Whiteboard: [reporter-external] [client-bounty-form] [verif?])
Attachments
(1 file)
22.24 KB,
text/html
|
Details |
NOTE: While I was investigating this issue, I noticed that a commit was pushed to mozilla-central on March 22 (two days ago as of now) fixing this vulnerability. However, I figured that it might've been found internally at Mozilla because of the crash reports Firefox sent when I had first triggered it, and I couldn't find anything in the bug bounty policy saying what to do in this case, so I'm just going ahead and submitting my writeup for bug bounty consideration anyway?? Sorry if this is the wrong thing to do, I really wasn't sure what to do in this situation...
Reproduction
Tested on Firefox for Android Stable, 123.1.0, running on a Pixel 6a with unmodified ROM.
To reproduce:
- Open Firefox for Android.
- Load attached PoC
wasm2c-poc.html
. - Hit the giant Start button, triggering repeated sound playback.
- The tab should crash very quickly with a SIGSEGV, often with a non-null fault address.
Explanation
In third_party\rlbox_wasm2c_sandbox\include\rlbox_wasm2c_sandbox.hpp
, the sandbox creation code (impl_create_sandbox
) can fail at several points using the FALLIBLE_DYNAMIC_CHECK
macro:
472: #define FALLIBLE_DYNAMIC_CHECK(infallible, cond, msg) \
473: if (infallible) { \
474: detail::dynamic_check(cond, msg); \
475: } else if (!(cond)) { \
476: impl_destroy_sandbox(); \
477: return false; \
478: }
479:
480: /**
481: * @brief creates the Wasm sandbox from the given shared library
482: *
483: * @param infallible if set to true, the sandbox aborts on failure. If false,
484: * the sandbox returns creation status as a return value
485: * @param custom_capacity allows optionally overriding the platform-specified
486: * maximum size of the wasm heap allowed for this sandbox instance.
487: * @return true when sandbox is successfully created. false when infallible is
488: * set to false and sandbox was not successfully created. If infallible is set
489: * to true, this function will never return false.
490: */
491: inline bool impl_create_sandbox(
492: bool infallible = true,
493: const w2c_mem_capacity* custom_capacity = nullptr)
494: {
495: FALLIBLE_DYNAMIC_CHECK(
496: infallible, instance_initialized == false, "Sandbox already initialized");
497:
498: bool minwasi_init_succeeded = true;
499:
500: std::call_once(rlbox_wasm2c_initialized, [&]() {
501: wasm_rt_init();
502: minwasi_init_succeeded = minwasi_init();
503: });
504:
505: FALLIBLE_DYNAMIC_CHECK(
506: infallible, minwasi_init_succeeded, "Could not initialize min wasi");
507:
508: const bool minwasi_init_inst_succeeded = minwasi_init_instance(&wasi_env);
509: FALLIBLE_DYNAMIC_CHECK(
510: infallible, minwasi_init_inst_succeeded, "Could not initialize min wasi instance");
511:
512: if (custom_capacity) {
513: FALLIBLE_DYNAMIC_CHECK(
514: infallible, custom_capacity->is_valid, "Invalid capacity");
515: }
516:
517: sandbox_memory_info = create_wasm2c_memory(
518: *RLBOX_WASM_MODULE_TYPE_CURR::initial_memory_pages, custom_capacity);
519: FALLIBLE_DYNAMIC_CHECK(infallible,
520: sandbox_memory_info.data != nullptr,
521: "Could not allocate a heap for the wasm2c sandbox");
522:
523: FALLIBLE_DYNAMIC_CHECK(infallible,
524: *RLBOX_WASM_MODULE_TYPE_CURR::is_memory_64 == 0,
525: "Does not support Wasm with memory64");
526:
527: const uint32_t max_table_size = 0xffffffffu; /* this means unlimited */
528: wasm_rt_allocate_funcref_table(
529: &sandbox_callback_table,
530: *RLBOX_WASM_MODULE_TYPE_CURR::initial_func_elements,
531: max_table_size);
532:
533: sandbox_memory_env.sandbox_memory_info = &sandbox_memory_info;
534: sandbox_memory_env.sandbox_callback_table = &sandbox_callback_table;
535: wasi_env.instance_memory = &sandbox_memory_info;
536: RLBOX_WASM_MODULE_TYPE_CURR::create_instance(
537: &wasm2c_instance, &sandbox_memory_env, &wasi_env);
538:
539: heap_base = reinterpret_cast<uintptr_t>(impl_get_memory_location());
540:
541: if constexpr (sizeof(uintptr_t) != sizeof(uint32_t)) {
542: // On larger platforms, check that the heap is aligned to the pointer size
543: // i.e. 32-bit pointer => aligned to 4GB. The implementations of
544: // impl_get_unsandboxed_pointer_no_ctx and
545: // impl_get_sandboxed_pointer_no_ctx below rely on this.
546: uintptr_t heap_offset_mask = std::numeric_limits<T_PointerType>::max();
547: FALLIBLE_DYNAMIC_CHECK(infallible,
548: (heap_base & heap_offset_mask) == 0,
549: "Sandbox heap not aligned to 4GB");
550: }
551:
552: instance_initialized = true;
553:
554: return true;
555: }
556:
557: #undef FALLIBLE_DYNAMIC_CHECK
If infallible
is false, the macro unilaterally calls impl_destroy_sandbox
before returning.
However, impl_destroy_sandbox
frees three member variables without checking that sandbox creation has gotten far enough to initialize them in the first place (lines 570-572):
559: inline void impl_destroy_sandbox()
560: {
561: if (return_slot_size) {
562: impl_free_in_sandbox(return_slot);
563: }
564:
565: if (instance_initialized) {
566: instance_initialized = false;
567: RLBOX_WASM_MODULE_TYPE_CURR::free_instance(&wasm2c_instance);
568: }
569:
570: destroy_wasm2c_memory(&sandbox_memory_info);
571: wasm_rt_free_funcref_table(&sandbox_callback_table);
572: minwasi_cleanup_instance(&wasi_env);
573: }
sandbox_callback_table
is of type wasm_rt_funcref_table_t
, containing a data pointer...
wasm_rt.h:
290: typedef struct {
291: /** The table element data, with an element count of `size`. */
292: wasm_rt_funcref_t* data;
293: [...]
298: } wasm_rt_funcref_table_t;
...which, still not initialized, wasm_rt_free_funcref_table
then free()
s.
I'm not completely sure what implications the calls to destroy_wasm2c_memory
and minwasi_cleanup_instance
have, but I haven't been able to trigger any crashes apparently associated with them so far.
Triggering
There are a few failure conditions in impl_create_sandbox
before the callback table is allocated, but the most likely one seems to be the heap allocation failing at line 519. I'm not sure how different operating systems behave when a process requests a bunch of virtual memory like this, but I've found at least one that it can realistically fail on: Android.
There's also at least one way, on Android, to trigger this condition in a content process through JavaScript: RLBoxSoundTouch
. Every AudioStream
initializes its own RLBoxSoundTouch
, which in turn initializes its own wasm2c sandbox, with infallible
set to false
:
File: media\libsoundtouch\src\RLBoxSoundTouch.cpp
13: RLBoxSoundTouch::RLBoxSoundTouch() {
14: #ifdef MOZ_WASM_SANDBOXING_SOUNDTOUCH
15: mSandbox.create_sandbox(false /* infallible */);
16: #else
17: mSandbox.create_sandbox();
18: #endif
(NB. If MOZ_WASM_SANDBOXING_SOUNDTOUCH
is not defined, infallible
defaults to true; I'm not sure under what build conditions this happens, if any, but it doesn't seem to on the official Firefox for Android build.)
Therefore, if we create and play a whole ton of audio streams with time-stretching enabled, a sandbox with 4GB virtual memory mapped in will be created for each of them. On Android, this quickly results in the allocation failing, triggering the aforementioned condition and thus free()
ing a pointer from uninitialized memory.
I initially found this a few days ago by accident on https://www.issmmbeatenyet.com/, where it can be triggered on Android by tapping the "YES" text very rapidly (which plays a time-stretched sound for each tap).
I've reduced it to a small HTML PoC, attached, consisting of an audio node with short base64-data-url-encoded OGG tone and a script to repeatedly clone and play it, similar to how issmmbeatenyet does. I've tested it on my stock Android Pixel 6a; the tab consistently crashes in seconds after hitting the Start button.
I haven't looked into the other uses of rlbox_wasm2c_sandbox
, but given that it is used in several other places, those may have security implications too.
Comment 1•1 year ago
|
||
Yes, this is an unusual situation, but thank you for filing the issue. Our bug bounty team will figure out what to do with it.
The bug that fixed at least some rlbox initialization issues this was bug 1885359. I did indeed file it based on crash reports, on March 14th. There was a bit of a spike of this crash on Nightly around then, from a variety of manufacturers like Samsung, Motorola, Xiaomi and Google so I'm not sure if this was related to your testing specifically.
Comment 2•1 year ago
|
||
Shravan, is this a duplicate of bug 1885359 or maybe there are new issues it raises? Thanks.
Reporter | ||
Comment 3•1 year ago
|
||
(In reply to Andrew McCreight [:mccr8] from comment #1)
Yes, this is an unusual situation, but thank you for filing the issue. Our bug bounty team will figure out what to do with it.
The bug that fixed at least some rlbox initialization issues this was bug 1885359. I did indeed file it based on crash reports, on March 14th. There was a bit of a spike of this crash on Nightly around then, from a variety of manufacturers like Samsung, Motorola, Xiaomi and Google so I'm not sure if this was related to your testing specifically.
In that case, it wasn't my crash reports that caused it after all -- I only first ran across the crash a few days ago. Thanks so much for your understanding, though! I'll leave this open and let the team figure out whether to close it as a duplicate or etc.
Comment 4•1 year ago
|
||
Ah, ok. Yeah I guess other people were hitting the same crash due to whatever change caused this to start happening, too, just a few weeks earlier.
Comment 5•1 year ago
|
||
@Andrew: Confirming that this is indeed a dup of Bug 1885359. Let me know if you need any more info from me.
Updated•1 year ago
|
Comment 7•1 year ago
|
||
(In reply to Natalie Raine from comment #0)
NOTE: While I was investigating this issue, I noticed that a commit was pushed to mozilla-central on March 22 (two days ago as of now) fixing this vulnerability. However, I figured that it might've been found internally at Mozilla because of the crash reports Firefox sent when I had first triggered it, and I couldn't find anything in the bug bounty policy saying what to do in this case, so I'm just going ahead and submitting my writeup for bug bounty consideration anyway?? Sorry if this is the wrong thing to do, I really wasn't sure what to do in this situation...
Although that's not what happened in this case, we thank you for submitting the bug anyway. Good reasons for doing so:
- We have a provision in our bug bounty program to split awards if more than one person reports the same bug within a collision window (72 hours). Mozilla-found bugs are no exception.¹
- sometimes testcases "appear" fixed when really they stopped working for unrelated reasons. If we can identify the root cause and see that it's still there and vulnerable under other conditions we will still award it. This point applies more to cases where you have not done this level of analysis; in this case you know the bug you identified had been fixed.
- it's unlikely crashes from a single researcher would be enough to trigger our detections, though it's not impossible. If you file your bug and link to some of your submitted crashes (available from the
about:crashes
page) we could look them up and see if those were the crashes we noticed.
The analysis you put into this bug is very good. Had Shravan not already found and fixed the bug this would have been valuable in fixing the bug and increased any bug bounty award. But it also takes time. If your priority is bug bounties we recommend that you file earlier, once you have a useful (reasonably reproducible) testcase. That will establish a point in time for your discovery that will be help you in case of collisions. You can still add your research findings later to increase the value of your submission. The largest part of a bounty award is based on the severity of the problem the reporter helped us eliminate; a well-researched write-up is a bonus. Don't spend so much time going for the bonus that you might miss getting any award at all if someone else reports more than a couple of days before you do.
¹ There is also 4 day exclusion window after a regression is introduced to give our normal processes a chance to find the bug.
Updated•11 months ago
|
Updated•7 months ago
|
Description
•