JS_ELEMENTS_HOLE Leak and Magic Leak to RCE
Categories
(Core :: JavaScript Engine, defect, P1)
Tracking
()
People
(Reporter: gaddofpwn, Assigned: jandem)
References
(Blocks 2 open bugs, Regression)
Details
(4 keywords, Whiteboard: [client-bounty-form][adv-main150.0.3+][adv-esr140.11+][adv-esr115.36+])
Attachments
(10 files, 1 obsolete file)
|
67 bytes,
text/html
|
Details | |
|
2.13 KB,
application/x-javascript
|
Details | |
|
31.17 KB,
application/x-javascript
|
Details | |
|
1.84 KB,
application/x-javascript
|
Details | |
|
48 bytes,
text/x-phabricator-request
|
tjr
:
sec-approval+
|
Details | Review |
|
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 |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-esr140+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-esr115+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
Details | Review |
JS_ELEMENTS_HOLE Leak and Magic Leak to RCE
Summary
SpiderMonkey's iterator-indices optimization can keep using a dense element
PropertyIndex after Object.defineProperty converts that dense element into a
sparse indexed property and a later addProperty hook failure removes only the
sparse property. The dense element remains JS_ELEMENTS_HOLE, iterator indices
remain available, and the object shape can be restored to the original shape.
Relevant Source
Iterator snapshot records dense element locations as PropertyIndex::ForElement.
Source: js/src/vm/Iteration.cpp
// Collect any dense elements from this object.
size_t firstElemIndex = props_.length();
size_t initlen = pobj->getDenseInitializedLength();
const Value* elements = pobj->getDenseElements();
bool elementsAreFrozen = pobj->denseElementsAreFrozen();
bool hasHoles = false;
for (uint32_t i = 0; i < initlen; ++i) {
if (elements[i].isMagic(JS_ELEMENTS_HOLE)) {
hasHoles = true;
} else {
PropertyIndex index = elementsAreFrozen ? PropertyIndex::Invalid()
: PropertyIndex::ForElement(i);
// Dense arrays never get so large that i would not fit into an
// integer id.
if (!enumerate<CheckForDuplicates>(cx, PropertyKey::Int(i),
/* enumerable = */ true, index)) {
return false;
}
}
}
Iterator creation marks indices as available when actual indices are copied.
Source: js/src/vm/Iteration.cpp
if (hasActualIndices) {
PropertyIndex* cursor = indicesBegin();
for (size_t i = 0; i < numProps; i++) {
*cursor++ = (*indices)[i];
}
flags_ |= Flags::IndicesAvailable;
}
For dense elements in for-in, the object is marked maybe-in-iteration, but
indices are still supported.
Source: js/src/vm/Iteration.cpp
if (obj->is<NativeObject>() &&
obj->as<NativeObject>().getDenseInitializedLength() > 0) {
if (forObjectKeys) {
supportsIndices = false;
} else {
obj->as<NativeObject>().markDenseElementsMaybeInIteration();
}
}
The Ion guard checks IndicesAvailable and object shape.
Source: js/src/jit/CodeGenerator.cpp
void CodeGenerator::emitIteratorHasIndicesAndBranch(Register iterator,
Register object,
Register temp,
Register temp2,
Label* ifFalse) {
// Check that the iterator has indices available.
Address nativeIterAddr(iterator,
PropertyIteratorObject::offsetOfIteratorSlot());
masm.loadPrivate(nativeIterAddr, temp);
masm.branchTest32(Assembler::Zero,
Address(temp, NativeIterator::offsetOfFlags()),
Imm32(NativeIterator::Flags::IndicesAvailable), ifFalse);
// Guard that the first shape stored in the iterator matches the current
// shape of the iterated object.
Address objShapeAddr(temp, NativeIterator::offsetOfObjectShape());
masm.loadPtr(objShapeAddr, temp);
masm.branchTestObjShape(Assembler::NotEqual, object, temp, temp2, object,
ifFalse);
}
The dense-element load checks bounds but not JS_ELEMENTS_HOLE.
Source: js/src/jit/CodeGenerator.cpp
Label indexOkay;
Address initLength(kindScratch, ObjectElements::offsetOfInitializedLength());
masm.branch32(Assembler::Above, initLength, indexScratch, &indexOkay);
masm.assumeUnreachable("Dense element out of bounds");
masm.bind(&indexOkay);
masm.loadValue(BaseObjectElementIndex(kindScratch, indexScratch), result);
masm.bind(&done);
Object.defineProperty enters AddOrChangeProperty for existing properties.
Source: js/src/vm/NativeObject.cpp
if (!AddOrChangeProperty<IsAddOrChange::Change>(cx, obj, id, desc, &prop)) {
return false;
}
Only default data properties use the dense fast path.
Source: js/src/vm/NativeObject.cpp
// Use dense storage for indexed properties where possible: when we have an
// integer key with default property attributes and are either adding a new
// property or changing a dense element.
PropertyFlags flags = ComputePropertyFlags(desc);
if (id.isInt() && flags == PropertyFlags::defaultDataPropFlags &&
(AddOrChange == IsAddOrChange::Add || existing->isDenseElement())) {
MOZ_ASSERT(!desc.isAccessorDescriptor());
MOZ_ASSERT(!obj->is<TypedArrayObject>());
uint32_t index = id.toInt();
DenseElementResult edResult = obj->ensureDenseElements(cx, index, 1);
if (edResult == DenseElementResult::Failure) {
return false;
}
if (edResult == DenseElementResult::Success) {
obj->setDenseElement(index, desc.value());
if (!CallAddPropertyHookDense(cx, obj, index, desc.value())) {
return false;
}
return true;
}
}
Accessor descriptors or non-default data descriptors add a sparse indexed
property first.
Source: js/src/vm/NativeObject.cpp
} else {
if (existing->isNativeProperty()) {
if (!NativeObject::changeProperty(cx, obj, id, flags, &slot)) {
return false;
}
Watchtower::watchPropertyValueChange<AllowGC::CanGC>(
cx, obj, id, desc.value(), existing->propertyInfo());
obj->setSlot(slot, desc.value());
} else {
if (!NativeObject::addProperty(cx, obj, id, flags, &slot)) {
return false;
}
obj->initSlot(slot, desc.value());
}
}
After adding the sparse indexed property, the dense element is removed.
Source: js/src/vm/NativeObject.cpp
// Clear any existing dense index after adding a sparse indexed property,
// and investigate converting the object to dense indexes.
if (id.isInt()) {
uint32_t index = id.toInt();
if constexpr (AddOrChange == IsAddOrChange::Add) {
MOZ_ASSERT(!obj->containsDenseElement(index));
} else {
obj->removeDenseElementForSparseIndex(index);
}
removeDenseElementForSparseIndex writes JS_ELEMENTS_HOLE and does not
disable iterator indices.
Source: js/src/vm/NativeObject-inl.h
inline void NativeObject::removeDenseElementForSparseIndex(uint32_t index) {
MOZ_ASSERT(containsPure(PropertyKey::Int(index)));
if (containsDenseElement(index)) {
setDenseElementHole(index);
}
}
Source: js/src/vm/NativeObject-inl.h
inline void NativeObject::setDenseElementHole(uint32_t index) {
markDenseElementsNotPacked();
setDenseElementUnchecked(index, MagicValue(JS_ELEMENTS_HOLE));
}
After the dense slot is changed to a hole, the generic add-property hook is
called.
Source: js/src/vm/NativeObject.cpp
if (desc.isDataDescriptor()) {
return CallAddPropertyHook(cx, obj, id, desc.value());
}
return CallAddPropertyHook(cx, obj, id, UndefinedHandleValue);
The hook call can fail before the class hook body is invoked.
Source: js/src/vm/NativeObject.cpp
static bool CallJSAddPropertyOp(JSContext* cx, JSAddPropertyOp op,
HandleObject obj, HandleId id, HandleValue v) {
AutoCheckRecursionLimit recursion(cx);
if (!recursion.check(cx)) {
return false;
}
cx->check(obj, id, v);
return op(cx, obj, id, v);
}
On failure, only the sparse property is removed. The dense element is not
restored.
Source: js/src/vm/NativeObject.cpp
static MOZ_ALWAYS_INLINE bool CallAddPropertyHook(JSContext* cx,
Handle<NativeObject*> obj,
HandleId id,
HandleValue value) {
// Ensure any wrapper is preserved first.
if (!PreserveAnyUnpreservedWrapper(cx, obj)) {
return false;
}
JSAddPropertyOp addProperty = obj->getClass()->getAddProperty();
if (MOZ_UNLIKELY(addProperty)) {
if (!CallJSAddPropertyOp(cx, addProperty, obj, id, value)) {
NativeObject::removeProperty(cx, obj, id);
return false;
}
}
return true;
}
Array objects have a built-in addProperty hook.
Source: js/src/builtin/Array.cpp
static bool array_addProperty(JSContext* cx, HandleObject obj, HandleId id,
HandleValue v) {
ArrayObject* arr = &obj->as<ArrayObject>();
uint32_t index;
if (!IdIsIndex(id, &index)) {
return true;
}
uint32_t length = arr->length();
if (index >= length) {
MOZ_ASSERT(arr->lengthIsWritable(),
"how'd this element get added if length is non-writable?");
arr->setLength(cx, index + 1);
}
return true;
}
Source: js/src/builtin/Array.cpp
static const JSClassOps ArrayObjectClassOps = {
array_addProperty, // addProperty
nullptr, // delProperty
nullptr, // enumerate
nullptr, // newEnumerate
nullptr, // resolve
nullptr, // mayResolve
nullptr, // finalize
Removing the just-added sparse property can restore the previous shape.
Source: js/src/vm/Shape.cpp
if (map->isShared()) {
// Fast path for removing the last property from a SharedPropMap. In this
// case we can just call getPrevious and then look up a shape for the
// resulting map/mapLength.
//
// Don't reuse a previously-used Shape if the object has an ObjectFuse and
// we are removing a tracked property, to avoid the following bug:
//
// 1) The object has Shape S1 and a property P that was marked NotConstant.
// 2) We attach a SetSlot IC stub that guards on the object + S1 and stores
// a new value for this property.
// 3) We remove property P (we are here now).
// 4) We add a new property P, reuse Shape S1, and mark P Constant.
// 5) We use the SetSlot IC stub again but this is invalid because P is
// still marked Constant.
if (propMap == map && propIndex == mapLength - 1 &&
!wasTrackedObjectFuseProp) {
MOZ_ASSERT(obj->getLastProperty().key() == id);
Rooted<SharedPropMap*> sharedMap(cx, map->asShared());
SharedPropMap::getPrevious(&sharedMap, &mapLength);
SharedShape* shape = obj->sharedShape();
SharedShape* newShape;
if (sharedMap) {
newShape = SharedShape::getPropMapShape(
cx, shape->base(), shape->numFixedSlots(), sharedMap, mapLength,
shape->objectFlags());
} else {
newShape = SharedShape::getInitialShape(
cx, shape->getObjectClass(), shape->realm(), shape->proto(),
shape->numFixedSlots(), shape->objectFlags());
}
if (!newShape) {
return false;
}
if (MOZ_LIKELY(prop.hasSlot())) {
if (MOZ_LIKELY(prop.slot() == newShape->slotSpan())) {
obj->setShapeAndRemoveLastSlot(cx, newShape, prop.slot());
return true;
}
// Uncommon case: the property is stored in a reserved slot.
// See NativeObject::addPropertyInReservedSlot.
MOZ_ASSERT(prop.slot() < JSCLASS_RESERVED_SLOTS(obj->getClass()));
obj->setSlot(prop.slot(), UndefinedValue());
}
obj->setShape(newShape);
return true;
}
Iterator deletion suppression disables indices, but this path is not used by
removeDenseElementForSparseIndex.
Source: js/src/vm/Iteration.cpp
static bool SuppressDeletedProperty(JSContext* cx, NativeIterator* ni,
HandleObject obj,
Handle<JSLinearString*> str) {
if (ni->objectBeingIterated() != obj) {
return true;
}
ni->disableIndices();
Source: js/src/vm/Iteration.h
void disableIndices() {
// Clear the IndicesAvailable flag so we won't use the indices on this
// iterator, and ensure IndicesSupported is cleared as well, so we don't
// re-request an iterator with indices. However, we leave the
// IndicesAllocated flag because we need to free them later, and skip them
// when looking for shapes.
flags_ &= ~(Flags::IndicesAvailable | Flags::IndicesSupported);
}
Exploit Primitive: CallArgs newTarget OOB Read
The leaked value is specifically MagicValue(JS_ELEMENTS_HOLE), not
MagicValue(JS_IS_CONSTRUCTING). The primitive depends on a mismatch in
CallArgs: CallArgsFromVp computes the stored constructing_ bit by checking
for the exact JS_IS_CONSTRUCTING magic value, while isConstructing() only
checks whether the this slot has any Magic tag.
Source: js/public/Value.h
enum JSWhyMagic {
/** a hole in a native object's elements */
JS_ELEMENTS_HOLE,
/** there is not a pending iterator value */
JS_NO_ITER_VALUE,
/** exception value thrown when closing a generator */
JS_GENERATOR_CLOSING,
/** used in debug builds to catch tracing errors */
JS_ARG_POISON,
/** an empty subnode in the AST serializer */
JS_SERIALIZE_NO_NODE,
/** magic value passed to natives to indicate construction */
JS_IS_CONSTRUCTING,
Source: js/public/Value.h
bool isMagic() const { return toTag() == JSVAL_TAG_MAGIC; }
bool isMagic(JSWhyMagic why) const {
if (!isMagic()) {
return false;
}
MOZ_RELEASE_ASSERT(whyMagic() == why);
return true;
}
// Like isMagic, but without the release assertion.
// Note that in release builds this will return *false* for
// non-matching magic values, because it is generally safer to
// ignore an unexpected magic value than to misinterpret it. See bug
// 2032226.
bool isMagicNoReleaseCheck(JSWhyMagic why) const {
MOZ_ASSERT_IF(isMagic(), whyMagic() == why);
return asBits_ == bitsFromTagAndPayload(JSVAL_TAG_MAGIC, uint32_t(why));
}
Source: js/public/CallArgs.h
bool isConstructing() const {
if (!argv_[-1].isMagic()) {
return false;
}
#ifdef JS_DEBUG
if (!this->usedRval()) {
CheckIsValidConstructible(calleev());
}
#endif
return true;
}
Source: js/public/CallArgs.h
MutableHandleValue newTarget() const {
MOZ_ASSERT(constructing_);
return MutableHandleValue::fromMarkedLocation(&this->argv_[argc_]);
}
Source: js/public/CallArgs.h
static CallArgs create(unsigned argc, Value* argv, bool constructing,
bool ignoresReturnValue = false) {
CallArgs args;
args.clearUsedRval();
args.argv_ = argv;
args.argc_ = argc;
args.constructing_ = constructing;
args.ignoresReturnValue_ = ignoresReturnValue;
Source: js/public/CallArgs.h
MOZ_ALWAYS_INLINE CallArgs CallArgsFromVp(unsigned argc, Value* vp) {
return CallArgs::create(argc, vp + 2,
vp[1].isMagicNoReleaseCheck(JS_IS_CONSTRUCTING));
}
For a native call where the leaked JS_ELEMENTS_HOLE is used as the this
slot, the state becomes:
argv_[-1] = MagicValue(JS_ELEMENTS_HOLE)
CallArgsFromVp:
constructing_ = isMagicNoReleaseCheck(JS_IS_CONSTRUCTING)
constructing_ = false
CallArgs::isConstructing:
argv_[-1].isMagic() = true
returns true
CallArgs::newTarget:
MOZ_ASSERT(constructing_) is debug-only
release build reads argv_[argc_]
The non-construct call frame does not reserve a new.target slot. Construct
calls reserve one extra slot, but ordinary calls only reserve callee, this, and
arguments.
Source: js/src/vm/Stack.h
// callee, this, arguments[, new.target iff constructing]
size_t len = 2 + argc + uint32_t(Construct);
MOZ_ASSERT(len > argc); // no overflow
if (!v_.resize(len)) {
return false;
}
*static_cast<JS::CallArgs*>(this) = CallArgsFromVp(argc, v_.begin());
this->constructing_ = Construct;
if (Construct) {
this->CallArgs::setThis(MagicValue(JS_IS_CONSTRUCTING));
}
obj_construct is a direct sink because it gates newTarget() with
args.isConstructing(). With JS_ELEMENTS_HOLE in the this slot, that guard
can pass even though the call was not constructed and constructing_ is false.
Source: js/src/builtin/Object.cpp
bool js::obj_construct(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
JSObject* obj;
if (args.isConstructing() &&
(&args.newTarget().toObject() != &args.callee())) {
RootedObject newTarget(cx, &args.newTarget().toObject());
obj = CreateThis(cx, newTarget);
} else if (args.length() > 0 && !args[0].isNullOrUndefined()) {
obj = ToObject(cx, args[0]);
} else {
/* Make an object whether this was called with 'new' or not. */
gc::AllocKind allocKind = NewObjectGCKind();
obj = NewPlainObjectWithAllocKind(cx, allocKind);
}
Primitive summary:
Input value:
MagicValue(JS_ELEMENTS_HOLE) is leaked as a JS Value
Native call state:
leaked value occupies argv_[-1] / thisv
CallArgsFromVp records constructing_ = false
CallArgs::isConstructing() returns true because the value is Magic
Read primitive:
args.newTarget() reads argv_[argc_]
for a non-construct call, argv_[argc_] is one Value past the arguments area
Exploit Chain: Worker Spray to UAF
The newTarget() primitive gives an out-of-bounds Value read from immediately
after the non-construct call arguments area. The exploitation strategy can use a
worker to shape that adjacent memory because the general allocator is not split
into isolated JavaScript heaps for the main thread and worker thread. The
allocator has a process-wide arena collection and ordinary malloc chooses an
arena through choose_arena() when no explicit private arena is supplied.
Source: memory/build/mozjemalloc.cpp
// Choose an arena based on a per-thread value.
static inline arena_t* choose_arena(size_t size) {
arena_t* ret = nullptr;
// We can only use TLS if this is a PIC library, since for the static
// library version, libc's malloc is used by TLS allocation, which
// introduces a bootstrapping issue.
if (size > kMaxQuantumClass) {
// Force the default arena for larger allocations.
ret = gArenas.GetDefault();
} else {
// Check TLS to see if our thread has requested a pinned arena.
ret = thread_arena.get();
// If ret is non-null, it must not be in the first page.
MOZ_DIAGNOSTIC_ASSERT_IF(ret, (size_t)ret >= gPageSize);
if (!ret) {
// Nothing in TLS. Pin this thread to the default arena.
ret = thread_local_arena(false);
}
}
MOZ_DIAGNOSTIC_ASSERT(ret);
return ret;
}
Source: memory/build/mozjemalloc.cpp
// The BaseAllocator class is a helper class that implements the base allocator
// functions (malloc, calloc, realloc, free, memalign) for a given arena,
// or an appropriately chosen arena (per choose_arena()) when none is given.
struct BaseAllocator {
Source: memory/build/mozjemalloc.cpp
inline void* BaseAllocator::malloc(size_t aSize) {
void* ret;
arena_t* arena;
if (!malloc_init()) {
ret = nullptr;
goto RETURN;
}
if (aSize == 0) {
aSize = 1;
}
// If mArena is non-null, it must not be in the first page.
MOZ_DIAGNOSTIC_ASSERT_IF(mArena, (size_t)mArena >= gPageSize);
arena = mArena ? mArena : choose_arena(aSize);
ret = arena->Malloc(aSize, /* aZero = */ false);
Worker execution uses its own WorkerJSContext created on the worker thread.
This gives the exploit separate JavaScript execution and GC scheduling from the
main thread, while allocations still go through the same process allocator
machinery.
Source: dom/workers/RuntimeService.cpp
class WorkerJSContext final : public mozilla::CycleCollectedJSContext {
public:
// The heap size passed here doesn't matter, we will change it later in the
// call to JS_SetGCParameter inside InitJSContextForWorker.
explicit WorkerJSContext(WorkerPrivate* aWorkerPrivate)
: mWorkerPrivate(aWorkerPrivate) {
Source: dom/workers/RuntimeService.cpp
{
nsCycleCollector_startup();
auto context = MakeUnique<WorkerJSContext>(mWorkerPrivate);
nsresult rv = context->Initialize(mParentRuntime);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
JSContext* cx = context->Context();
if (!InitJSContextForWorker(mWorkerPrivate, cx)) {
return NS_ERROR_FAILURE;
}
The main-thread and worker GC entry points are distinct. The same API-level GC
request dispatches to nsJSContext::GarbageCollectNow on the main thread, but
to WorkerPrivate::GarbageCollectInternal on a worker thread.
Source: dom/base/TestUtils.cpp
NS_DispatchToCurrentThread(
NS_NewCancelableRunnableFunction("TestUtils::Gc", [promise] {
if (NS_IsMainThread()) {
nsJSContext::GarbageCollectNow(JS::GCReason::DOM_TESTUTILS,
nsJSContext::NonShrinkingGC);
nsJSContext::CycleCollectNow(CCReason::API);
} else {
WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
workerPrivate->GarbageCollectInternal(workerPrivate->GetJSContext(),
false /* shrinking */,
false /* collect children */);
workerPrivate->CycleCollectInternal(false);
}
promise->MaybeResolveWithUndefined();
}));
Source: dom/base/nsJSEnvironment.cpp
static void GarbageCollectImpl(JS::GCReason aReason,
nsJSContext::IsShrinking aShrinking,
const JS::SliceBudget& aBudget) {
AUTO_PROFILER_LABEL_DYNAMIC_CSTR_NONSENSITIVE(
"nsJSContext::GarbageCollectNow", GCCC, JS::ExplainGCReason(aReason));
bool wantIncremental = !aBudget.isUnlimited();
// We use danger::GetJSContext() since AutoJSAPI will assert if the current
// thread's context is null (such as during shutdown).
JSContext* cx = danger::GetJSContext();
Source: dom/workers/WorkerPrivate.cpp
void WorkerPrivate::GarbageCollectInternal(JSContext* aCx, bool aShrinking,
bool aCollectChildren) {
// Perform GC followed by CC (the CC is triggered by
// WorkerJSRuntime::CustomGCCallback at the end of the collection).
auto data = mWorkerThreadAccessible.Access();
if (!GlobalScope()) {
// We haven't compiled anything yet. Just bail out.
return;
}
Exploit-chain summary:
Primitive:
args.newTarget() gives a one-Value OOB read from a non-construct call frame
Allocator condition:
worker and main-thread allocations are not isolated into separate heaps
ordinary allocations use the shared process allocator arena machinery
worker-side spray can influence adjacent or reusable allocator state
GC condition:
main-thread GC and worker GC are driven through distinct entry points
worker GC can reclaim worker-owned objects independently of main-thread GC
UAF connection:
worker-side allocation/free pressure shapes memory visible to the OOB read
independent worker GC provides a controlled lifetime break
the OOB read can then observe stale or reused Value/object memory as a UAF
Final State
NativeIterator:
IndicesAvailable remains set
saved PropertyIndex remains ForElement(0)
Array object:
shape can be restored to the shape captured by the iterator
dense element 0 is JS_ELEMENTS_HOLE
sparse property "0" has been removed by rollback
Ion fast path:
IndicesAvailable check passes
shape guard passes
dense element bounds check passes
JS_ELEMENTS_HOLE is loaded without a hole check
Exploit primitive:
JS_ELEMENTS_HOLE is a Magic value but not JS_IS_CONSTRUCTING
CallArgsFromVp sets constructing_ to false
CallArgs::isConstructing() still returns true for any Magic value
args.newTarget() can read argv_[argc_] in a non-construct call
Exploit chain:
the newTarget OOB read is paired with worker-side heap shaping
worker and main-thread allocations use shared process allocator machinery
worker GC and main-thread GC are driven through separate paths
this allows a worker-controlled lifetime break to be observed as UAF
Suggested Fix
When removeDenseElementForSparseIndex writes JS_ELEMENTS_HOLE,
invalidate active iterator indices for the object.
Make CallArgs::isConstructing() use the same exact construction marker check
as CallArgsFromVp, or return the stored constructing_ bit instead of checking
argv_[-1].isMagic().
PoC Status
The PoC was written as a full chain for the Firefox 150.0.2 Windows build,
using the shellcode from bug 2038573.
Updated•4 months ago
|
| Assignee | ||
Updated•4 months ago
|
Updated•4 months ago
|
| Assignee | ||
Comment 3•4 months ago
|
||
This asserts in a Linux debugopt shell for me when I run it with --no-threads.
| Assignee | ||
Comment 4•4 months ago
|
||
This matches CallAddPropertyHookDense.
| Assignee | ||
Comment 5•4 months ago
•
|
||
I've posted a patch that does two things:
It inlines the ArrayObject addProperty hook code in CallAddPropertyHook similar to what we already do in CallAddPropertyHookDense. This ensures CallAddPropertyHook is infallible for arrays because we no longer have the unnecessary recursion check there. The other remaining places where we still use addPropertyhooks are XPConnect objects (see bug 1973249).
The patch also changes CodeGenerator::visitLoadSlotByIteratorIndexCommon to crash safely if we read the hole MagicValue, in addition to the bounds check we already had there.
There's more work to do in follow-up bugs but for now these two changes are relatively small and safe.
Updated•4 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Updated•4 months ago
|
Updated•4 months ago
|
| Assignee | ||
Comment 6•4 months ago
|
||
Comment on attachment 9585091 [details]
(secure)
Security Approval Request
- How easily could an exploit be constructed based on the patch?: Not very easily but maybe with AI analysis..
- 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?: all
- If not all supported branches, which bug introduced the flaw?: None
- Do you have backports for the affected branches?: Yes
- If not, how different, hard to create, and risky will they be?: Should be easy to backport.
- How likely is this patch to cause regressions; how much testing does it need?: Unlikely. The inlining part is very safe and the JIT part turns a bad scenario into a safe crash. If we hit this before we'd likely have crashed later.
- Is the patch ready to land after security approval is given?: Yes
- Is Android affected?: Yes
Updated•4 months ago
|
Comment 8•4 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: Security bugs.
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Minimal fix for the security bug. In cases that would very likely have crashed before, it'll now introduce a safe crash.
- String changes made/needed?: N/A
- Is Android affected?: yes
| Assignee | ||
Comment 9•4 months ago
|
||
This matches CallAddPropertyHookDense.
Original Revision: https://phabricator.services.mozilla.com/D299696
Comment 10•4 months ago
|
||
firefox-release Uplift Approval Request
- User impact if declined/Reason for urgency: Security bugs.
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Minimal fix for the security bug. In cases that would very likely have crashed before, it'll now introduce a safe crash.
- String changes made/needed?: N/A
- Is Android affected?: yes
| Assignee | ||
Comment 11•4 months ago
|
||
This matches CallAddPropertyHookDense.
Original Revision: https://phabricator.services.mozilla.com/D299696
Comment 12•4 months ago
|
||
:jandem, this will also need uplift requests for ESR115 and ESR140. You might already be working on them, but adding a need-info just in case.
Comment 13•4 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined/Reason for urgency: Security bugs.
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Minimal fix for the security bug. In cases that would very likely have crashed before, it'll now introduce a safe crash.
- String changes made/needed?: N/A
- Is Android affected?: yes
| Assignee | ||
Comment 14•4 months ago
|
||
This matches CallAddPropertyHookDense.
Original Revision: https://phabricator.services.mozilla.com/D299696
Comment 15•4 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined/Reason for urgency: Security bugs.
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Minimal fix for the security bug. In cases that would very likely have crashed before, it'll now introduce a safe crash.
- String changes made/needed?: N/A
- Is Android affected?: yes
| Assignee | ||
Comment 16•4 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D299696
Updated•4 months ago
|
Updated•4 months ago
|
Comment 17•4 months ago
|
||
firefox-esr115 Uplift Approval Request
- User impact if declined/Reason for urgency: Security bugs.
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Minimal fix for the security bug. In cases that would very likely have crashed before, it'll now introduce a safe crash.
- String changes made/needed?: N/A
- Is Android affected?: yes
| Assignee | ||
Comment 18•4 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D299696
| Assignee | ||
Comment 19•4 months ago
|
||
(In reply to Donal Meehan [:dmeehan] from comment #12)
:jandem, this will also need uplift requests for ESR115 and ESR140. You might already be working on them, but adding a need-info just in case.
Yeah we were still working on this :) ESR140/ESR115 needed a slightly different approach because there we still have DOM objects with addProperty hooks too. Like the array hook, these DOM hooks are infallible, so not doing the overrecursion check seemed the safest option. These hooks can't call each other recursively.
Updated•4 months ago
|
Updated•4 months ago
|
Comment 20•4 months ago
|
||
| uplift | ||
Updated•4 months ago
|
Comment 21•4 months ago
|
||
| 150.0.3 uplift | ||
Comment 22•4 months ago
|
||
worker and main-thread allocations are not isolated into separate heaps
Is this something we want to fix? Should we file a follow-up?
Updated•4 months ago
|
Updated•4 months ago
|
| Assignee | ||
Comment 23•4 months ago
|
||
(In reply to Simon Friedberger [:simonf] from comment #22)
Is this something we want to fix? Should we file a follow-up?
It's complicated because some memory sharing is necessary to efficiently pass eg ArrayBuffers and string buffers between threads. I think this bug would have been exploitable without workers too because you could use heap spraying/grooming on a single thread. The write-up mentions jemalloc but from reading the code it (also) might be relying on GC allocator behavior.
Heap partitioning (bug 1052575) seems interesting but I lack the expertise to suggest meaningful mitigations to deploy in Firefox. Input from the security team on this would be valuable.
| Assignee | ||
Comment 24•4 months ago
|
||
Comment 25•4 months ago
|
||
Updated•4 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Comment 26•4 months ago
|
||
| uplift | ||
Updated•4 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Comment 27•4 months ago
|
||
| uplift | ||
Updated•4 months ago
|
Comment 28•4 months ago
|
||
| 140.10.3 uplift | ||
Updated•4 months ago
|
Comment 29•4 months ago
|
||
| 115.35.3 uplift | ||
Updated•4 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•17 days ago
|
Comment 30•12 days ago
|
||
Updated•12 days ago
|
Comment 31•12 days ago
|
||
| bugherder | ||
Description
•