CVE-2024-8385
Overview
Background
- WASM GC
- A SpiderMonkey feature that lets WebAssembly modules define managed heap objects such as structs and arrays, whose fields are described by type metadata.
- `StructType`
- The type descriptor for a WASM struct that holds a
fields_vector describing each field’sStorageTypeand its byte layout within inline or outline storage. - `FieldType` / `StructField`
- Two distinct representations of a field descriptor; the fix migrates code to
FieldTypeand computes offsets viastructType.fieldOffset()rather than reading a rawoffsetmember. - `BaseCompiler`
- The WASM baseline (fast, non-optimizing) code generator whose
emit*methods lower GC struct/array opcodes directly to machine code.
Root Cause Analysis
The baseline compiler’s GC struct and array emitters accessed field and element metadata through a stale representation, reading arrayType.elementType_ directly and computing a struct field’s location from a raw field.offset / structType.fields_[fieldIndex].offset member instead of the canonical structType.fieldOffset(fieldIndex) accessor. Because StructField and ArrayType/FieldType layouts diverged, the raw elementType_ and offset values no longer described the true in-memory layout of the object, so the invariant that the compiler’s computed offset and element size match the actual allocated object layout was violated. An attacker-controlled combination of struct/array type definitions could therefore make emitStructGet, emitStructSet, emitArrayGet, and emitArraySet read or write at a mismatched offset or with a mismatched element size, producing type confusion between a scalar and a reference field.
The fix replaces every direct elementType_ access with the elementType() accessor and every raw offset read with structType.fieldOffset(fieldIndex), and retypes the field variables from StructField to FieldType, so all layout decisions flow through the single authoritative source.
offset/elementType_ field descriptor instead of the canonical layout accessors, allowing a scalar field to be treated as a reference (or vice versa); the fix routes every layout query through elementType() and fieldOffset(), eliminating the divergence that enabled type confusion.Attack Path
- Define crafted GC types
The attacker authors a WASM module declaring
StructTypeand array types whoseFieldTypelayout differs from what the staleStructField::offset/elementType_members report. - Trigger baseline compilation
Execution routes the module through
BaseCompiler, whoseemitStructNew,emitStructGet,emitStructSet, and array emitters lower field accesses using the mismatched raw offsets and element size. - Induce type confusion
A read or write via
emitStructGet/emitArraySetlands at the wrong offset or misinterprets a reference slot as a scalar (or the reverse), so a controlled integer is treated as a managed pointer. - Corrupt object state
By writing a chosen value into a slot the engine believes holds a
ref, the attacker forges an object pointer or otherwise gains an out-of-bounds/mistyped access primitive. - Escalate The forged pointer is leveraged into arbitrary read/write within the content process, leading to code execution.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
whilejs/src/wasm/WasmBaselineCompile.cpp |
modified | |
ifjs/src/wasm/WasmBaselineCompile.cpp |
modified | |
forjs/src/wasm/WasmDump.cpp |
modified | |
ifjs/src/wasm/WasmDump.cpp |
modified | |
forjs/src/wasm/WasmGcObject.cpp |
modified |
Files Changed
js/src/wasm/WasmBaselineCompile.cppjs/src/wasm/WasmDump.cppjs/src/wasm/WasmGcObject-inl.hjs/src/wasm/WasmGcObject.cppjs/src/wasm/WasmGcObject.hjs/src/wasm/WasmInstance.cppjs/src/wasm/WasmIonCompile.cppjs/src/wasm/WasmOpIter.hjs/src/wasm/WasmPI.cppjs/src/wasm/WasmSerialize.cppjs/src/wasm/WasmTypeDef.cppjs/src/wasm/WasmTypeDef.hjs/src/wasm/WasmValidate.cpp
Audit Directions
- Duplicate layout sourcesSearch for any remaining direct reads of
elementType_or a rawoffset/fields_[...].offsetmember and require all layout queries to go throughelementType()andfieldOffset(). - Baseline vs. Ion parityCompare
BaseCompilerGC emitters againstWasmIonCompile.cppto confirm both compilers derive struct/array offsets and element sizes from the same canonical accessors. - Type-descriptor migrationsWhen
StructField,FieldType, andArrayTyperepresentations are refactored, audit every consumer for stale members that were left pointing at the old layout, since divergence there reintroduces type confusion.
Patch
diff --git a/js/src/wasm/WasmBaselineCompile.cpp b/js/src/wasm/WasmBaselineCompile.cpp
index 3e92169aa25..816309e359d 100644
--- a/js/src/wasm/WasmBaselineCompile.cpp
+++ b/js/src/wasm/WasmBaselineCompile.cpp
@@ -7376,7 +7376,7 @@ bool BaseCompiler::emitGcArraySet(RegRef object, RegPtr data, RegI32 index,
// shift immediate. If not we shift the index manually and then unshift
// it after the store. We don't use an extra register for this because we
// don't have any to spare on x86.
- uint32_t shift = arrayType.elementType_.indexingShift();
+ uint32_t shift = arrayType.elementType().indexingShift();
Scale scale;
bool shiftedIndex = false;
if (IsShiftInScaleRange(shift)) {
@@ -7393,9 +7393,9 @@ bool BaseCompiler::emitGcArraySet(RegRef object, RegPtr data, RegI32 index,
});
// Easy path if the field is a scalar
- if (!arrayType.elementType_.isRefRepr()) {
+ if (!arrayType.elementType().isRefRepr()) {
emitGcSetScalar<BaseIndex, NoNullCheck>(BaseIndex(data, index, scale, 0),
- arrayType.elementType_, value);
+ arrayType.elementType(), value);
return true;
}
@@ -7537,9 +7537,9 @@ bool BaseCompiler::emitStructNew() {
uint32_t fieldIndex = structType.fields_.length();
while (fieldIndex-- > 0) {
- const StructField& field = structType.fields_[fieldIndex];
+ const FieldType& field = structType.fields_[fieldIndex];
StorageType type = field.type;
- uint32_t fieldOffset = field.offset;
+ uint32_t fieldOffset = structType.fieldOffset(fieldIndex);
bool areaIsOutline;
uint32_t areaOffset;
@@ -7626,7 +7626,7 @@ bool BaseCompiler::emitStructGet(FieldWideningOp wideningOp) {
// Decide whether we're accessing inline or outline, and at what offset
StorageType fieldType = structType.fields_[fieldIndex].type;
- uint32_t fieldOffset = structType.fields_[fieldIndex].offset;
+ uint32_t fieldOffset = structType.fieldOffset(fieldIndex);
bool areaIsOutline;
uint32_t areaOffset;
@@ -7667,11 +7667,11 @@ bool BaseCompiler::emitStructSet() {
}
const StructType& structType = (*codeMeta_.types)[typeIndex].structType();
- const StructField& structField = structType.fields_[fieldIndex];
+ const FieldType& structField = structType.fields_[fieldIndex];
// Decide whether we're accessing inline or outline, and at what offset
StorageType fieldType = structType.fields_[fieldIndex].type;
- uint32_t fieldOffset = structType.fields_[fieldIndex].offset;
+ uint32_t fieldOffset = structType.fieldOffset(fieldIndex);
bool areaIsOutline;
uint32_t areaOffset;
@@ -7845,14 +7845,14 @@ bool BaseCompiler::emitArrayNew() {
// Reserve this register early if we will need it so that it is not taken by
// any register used in this function.
- if (arrayType.elementType_.isRefRepr()) {
+ if (arrayType.elementType().isRefRepr()) {
needPtr(RegPtr(PreBarrierReg));
}
RegRef object = needRef();
RegI32 numElements = popI32();
if (!emitArrayAlloc<false>(typeIndex, object, numElements,
- arrayType.elementType_.size())) {
+ arrayType.elementType().size())) {
return false;
}
@@ -7865,7 +7865,7 @@ bool BaseCompiler::emitArrayNew() {
numElements = emitGcArrayGetNumElements<NoNullCheck>(object);
// Free the barrier reg after we've allocated all registers
- if (arrayType.elementType_.isRefRepr()) {
+ if (arrayType.elementType().isRefRepr()) {
freePtr(RegPtr(PreBarrierReg));
}
@@ -7913,14 +7913,14 @@ bool BaseCompiler::emitArrayNewFixed() {
// Reserve this register early if we will need it so that it is not taken by
// any register used in this function.
- bool avoidPreBarrierReg = arrayType.elementType_.isRefRepr();
+ bool avoidPreBarrierReg = arrayType.elementType().isRefRepr();
if (avoidPreBarrierReg) {
needPtr(RegPtr(PreBarrierReg));
}
RegRef object = needRef();
if (!emitArrayAllocFixed<false>(typeIndex, object, numElements,
- arrayType.elementType_.size())) {
+ arrayType.elementType().size())) {
return false;
}
@@ -7983,7 +7983,7 @@ bool BaseCompiler::emitArrayNewDefault() {
RegRef object = needRef();
RegI32 numElements = popI32();
if (!emitArrayAlloc<true>(typeIndex, object, numElements,
- arrayType.elementType_.size())) {
+ arrayType.elementType().size())) {
return false;
}
@@ -8100,14 +8100,14 @@ bool BaseCompiler::emitArrayGet(FieldWideningOp wideningOp) {
RegPtr rdata = emitGcArrayGetData<NoNullCheck>(rp);
// Load the value
- uint32_t shift = arrayType.elementType_.indexingShift();
+ uint32_t shift = arrayType.elementType().indexingShift();
if (IsShiftInScaleRange(shift)) {
emitGcGet<BaseIndex, NoNullCheck>(
- arrayType.elementType_, wideningOp,
+ arrayType.elementType(), wideningOp,
BaseIndex(rdata, index, ShiftToScale(shift), 0));
} else {
masm.lshiftPtr(Imm32(shift), index);
- emitGcGet<BaseIndex, NoNullCheck>(arrayType.elementType_, wideningOp,
+ emitGcGet<BaseIndex, NoNullCheck>(arrayType.elementType(), wideningOp,
BaseIndex(rdata, index, TimesOne, 0));
}
@@ -8133,7 +8133,7 @@ bool BaseCompiler::emitArraySet() {
// Reserve this register early if we will need it so that it is not taken by
// any register used in this function.
- if (arrayType.elementType_.isRefRepr()) {
+ if (arrayType.elementType().isRefRepr()) {
needPtr(RegPtr(PreBarrierReg));
}
@@ -8152,7 +8152,7 @@ bool BaseCompiler::emitArraySet() {
RegPtr rdata = emitGcArrayGetData<NoNullCheck>(rp);
// Free the barrier reg after we've allocated all registers
- if (arrayType.elementType_.isRefRepr()) {
+ if (arrayType.elementType().isRefRepr()) {
freePtr(RegPtr(PreBarrierReg));
}
@@ -8229,7 +8229,7 @@ bool BaseCompiler::emitArrayFill() {
const TypeDef& typeDef = codeMeta_.types->type(typeIndex);
const ArrayType& arrayType = typeDef.arrayType();
- StorageType elementType = arrayType.elementType_;
+ StorageType elementType = arrayType.elementType();
// On x86 (32-bit), we are very short of registers, hence the code
// generation scheme is less straightforward than it might otherwise be.
@@ -8388,7 +8388,7 @@ bool BaseCompiler::emitArrayFill() {
// Currently `rdata` points at the start of the array data area. Move it
// forwards by `index` units so as to make it point at the start of the area
// to be filled.
- uint32_t shift = arrayType.elementType_.indexingShift();
+ uint32_t shift = arrayType.elementType().indexingShift();
if (shift > 0) {
masm.lshift32(Imm32(shift), index);
// `index` is a 32 bit value, so we must zero-extend it to 64 bits before
diff --git a/js/src/wasm/WasmDump.cpp b/js/src/wasm/WasmDump.cpp
index f92125077aa..be0c08ebb21 100644
--- a/js/src/wasm/WasmDump.cpp
+++ b/js/src/wasm/WasmDump.cpp
@@ -193,7 +193,7 @@ void wasm::Dump(const StructType& structType, IndentedPrinter& out) {
out.printf("(struct\n");
{
IndentedPrinter::AutoIndent innerIndent(out);
- for (const StructField& field : structType.fields_) {
+ for (const FieldType& field : structType.fields_) {
out.printf("(field ");
if (field.isMutable) {
out.printf("(mut ");
@@ -216,11 +216,11 @@ void wasm::Dump(const ArrayType& arrayType) {
void wasm::Dump(const ArrayType& arrayType, IndentedPrinter& out) {
out.printf("(array ");
- if (arrayType.isMutable_) {
+ if (arrayType.isMutable()) {
out.printf("(mut ");
}
- Dump(arrayType.elementType_, out);
- if (arrayType.isMutable_) {
+ Dump(arrayType.elementType(), out);
+ if (arrayType.isMutable()) {
out.printf(")");
}
out.printf(")\n");
diff --git a/js/src/wasm/WasmGcObject-inl.h b/js/src/wasm/WasmGcObject-inl.h
index c12b30a0588..29f42e617fb 100644
--- a/js/src/wasm/WasmGcObject-inl.h
+++ b/js/src/wasm/WasmGcObject-inl.h
@@ -171,7 +171,7 @@ inline gc::AllocKind WasmArrayObject::allocKind() const {
if (isDataInline()) {
// numElements_ was validated to not overflow when constructing this object
uint32_t storageBytes = calcStorageBytesUnchecked(
- typeDef().arrayType().elementType_.size(), numElements_);
+ typeDef().arrayType().elementType().size(), numElements_);
return allocKindForIL(storageBytes);
}
@@ -338,7 +338,7 @@ MOZ_ALWAYS_INLINE WasmArrayObject* WasmArrayObject::createArray(
JSContext* cx, wasm::TypeDefInstanceData* typeDefData,
js::gc::Heap initialHeap, uint32_t numElements) {
MOZ_ASSERT(typeDefData->arrayElemSize ==
- typeDefData->typeDef->arrayType().elementType_.size());
+ typeDefData->typeDef->arrayType().elementType().size());
mozilla::CheckedUint32 storageBytes =
calcStorageBytesChecked(typeDefData->arrayElemSize, numElements);
if (!storageBytes.isValid() ||
diff --git a/js/src/wasm/WasmGcObject.cpp b/js/src/wasm/WasmGcObject.cpp
index ee4c2f30dae..2faaad4c3b0 100644
--- a/js/src/wasm/WasmGcObject.cpp
+++ b/js/src/wasm/WasmGcObject.cpp
@@ -238,9 +238,8 @@ bool WasmGcObject::lookUpProperty(JSContext* cx, Handle<WasmGcObject*> obj,
JSMSG_WASM_OUT_OF_BOUNDS);
return false;
}
- const StructField& field = structType.fields_[index];
- offset->set(field.offset);
- *type = field.type;
+ offset->set(structType.fieldOffset(index));
+ *type = structType.fields_[index].type;
return true;
}
case wasm::TypeDefKind::Array: {
@@ -255,13 +254,13 @@ bool WasmGcObject::lookUpProperty(JSContext* cx, Handle<WasmGcObject*> obj,
return false;
}
uint64_t scaledIndex =
- uint64_t(index) * uint64_t(arrayType.elementType_.size());
+ uint64_t(index) * uint64_t(arrayType.elementType().size());
if (scaledIndex >= uint64_t(UINT32_MAX)) {
// It's unrepresentable as an WasmGcObject::PropOffset. Give up.
return false;
}
offset->set(uint32_t(scaledIndex));
- *type = arrayType.elementType_;
+ *type = arrayType.elementType();
return true;
}
default:
@@ -359,12 +358,12 @@ void WasmArrayObject::obj_trace(JSTracer* trc, JSObject* object) {
const auto& typeDef = arrayObj.typeDef();
const auto& arrayType = typeDef.arrayType();
- if (!arrayType.elementType_.isRefRepr()) {
+ if (!arrayType.elementType().isRefRepr()) {
return;
}
uint32_t numElements = arrayObj.numElements_;
- uint32_t elemSize = arrayType.elementType_.size();
+ uint32_t elemSize = arrayType.elementType().size();
for (uint32_t i = 0; i < numElements; i++) {
AnyRef* elementPtr = reinterpret_cast<AnyRef*>(data + i * elemSize);
TraceManuallyBarrieredEdge(trc, elementPtr, "wasm-array-element");
@@ -389,7 +388,7 @@ void WasmArrayObject::obj_finalize(JS::GCContext* gcx, JSObject* object) {
// arrayObj.numElements_ was validated to not overflow when constructing the
// array
size_t trailerSize = calcStorageBytesUnchecked(
- typeDef.arrayType().elementType_.size(), arrayObj.numElements_);
+ typeDef.arrayType().elementType().size(), arrayObj.numElements_);
// Ensured by WasmArrayObject::createArrayNonEmpty.
MOZ_RELEASE_ASSERT(trailerSize <= size_t(MaxArrayPayloadBytes));
gcx->removeCellMemory(&arrayObj, trailerSize + TrailerBlockOverhead,
@@ -420,7 +419,7 @@ size_t WasmArrayObject::obj_moved(JSObject* obj, JSObject* old) {
// arrayObj.numElements_ was validated not to overflow when constructing
// the array
size_t trailerSize = calcStorageBytesUnchecked(
- typeDef.arrayType().elementType_.size(), arrayObj.numElements_);
+ typeDef.arrayType().elementType().size(), arrayObj.numElements_);
// Ensured by WasmArrayObject::createArrayOOL.
MOZ_RELEASE_ASSERT(trailerSize <= size_t(MaxArrayPayloadBytes));
nursery.trackTrailerOnPromotion(arrayObj.dataHeader(), obj, trailerSize,
@@ -434,20 +433,20 @@ size_t WasmArrayObject::obj_moved(JSObject* obj, JSObject* old) {
void WasmArrayObject::storeVal(const Val& val, uint32_t itemIndex) {
const ArrayType& arrayType = typeDef().arrayType();
- size_t elementSize = arrayType.elementType_.size();
+ size_t elementSize = arrayType.elementType().size();
MOZ_ASSERT(itemIndex < numElements_);
uint8_t* data = data_ + elementSize * itemIndex;
- WriteValTo(val, arrayType.elementType_, data);
+ WriteValTo(val, arrayType.elementType(), data);
}
void WasmArrayObject::fillVal(const Val& val, uint32_t itemIndex,
uint32_t len) {
const ArrayType& arrayType = typeDef().arrayType();
- size_t elementSize = arrayType.elementType_.size();
+ size_t elementSize = arrayType.elementType().size();
uint8_t* data = data_ + elementSize * itemIndex;
MOZ_ASSERT(itemIndex <= numElements_ && len <= numElements_ - itemIndex);
for (uint32_t i = 0; i < len; i++) {
- WriteValTo(val, arrayType.elementType_, data);
+ WriteValTo(val, arrayType.elementType(), data);
data += elementSize;
}
}
@@ -513,9 +512,10 @@ bool WasmStructObject::getField(JSContext* cx, uint32_t index,
MutableHandle<Value> val) {
const StructType& resultType = typeDef().structType();
MOZ_ASSERT(index <= resultType.fields_.length());
- const StructField& field = resultType.fields_[index];
+ const FieldType& field = resultType.fields_[index];
+ uint32_t fieldOffset = resultType.fieldOffset(index);
StorageType ty = field.type.storageType();
- return ToJSValue(cx, fieldOffsetToAddress(ty, field.offset), ty, val);
+ return ToJSValue(cx, fieldOffsetToAddress(ty, fieldOffset), ty, val);
}
/* static */
@@ -578,7 +578,7 @@ size_t WasmStructObject::obj_moved(JSObject* obj, JSObject* old) {
void WasmStructObject::storeVal(const Val& val, uint32_t fieldIndex) {
const StructType& structType = typeDef().structType();
StorageType fieldType = structType.fields_[fieldIndex].type;
- uint32_t fieldOffset = structType.fields_[fieldIndex].offset;
+ uint32_t fieldOffset = structType.fieldOffset(fieldIndex);
MOZ_ASSERT(fieldIndex < structType.fields_.length());
bool areaIsOutline;
diff --git a/js/src/wasm/WasmGcObject.h b/js/src/wasm/WasmGcObject.h
index b68679c5c72..e09596aefa2 100644
--- a/js/src/wasm/WasmGcObject.h
+++ b/js/src/wasm/WasmGcObject.h
@@ -86,8 +86,8 @@ class WasmGcObject : public JSObject {
// single contiguous area of memory:
//
// * If the object in question is a WasmStructObject, it is the value of
- // `wasm::StructField::offset` for the relevant field, without regard to
- // the inline/outline split.
+ // `wasm::StructType::fieldOffset()` for the relevant field, without regard
+ // to the inline/outline split.
//
// * If the object in question is a WasmArrayObject, then
// - u32 == UINT32_MAX (0xFFFF'FFFF) means the "length" property
diff --git a/js/src/wasm/WasmInstance.cpp b/js/src/wasm/WasmInstance.cpp
index fe613b248b3..6c4fc95416a 100644
--- a/js/src/wasm/WasmInstance.cpp
+++ b/js/src/wasm/WasmInstance.cpp
@@ -1514,7 +1514,7 @@ static bool ArrayCopyFromData(JSContext* cx, Handle<WasmArrayObject*> arrayObj,
// Compute the number of bytes to copy, ensuring it's below 2^32.
CheckedUint32 numBytesToCopy =
CheckedUint32(numElements) *
- CheckedUint32(typeDef->arrayType().elementType_.size());
+ CheckedUint32(typeDef->arrayType().elementType().size());
if (!numBytesToCopy.isValid()) {
// Because the request implies that 2^32 or more bytes are to be copied.
ReportTrapError(cx, JSMSG_WASM_OUT_OF_BOUNDS);
@@ -1665,7 +1665,7 @@ static bool ArrayCopyFromElem(JSContext* cx, Handle<WasmArrayObject*> arrayObj,
// Any data coming from an element segment will be an AnyRef. Writes into
// array memory are done with raw pointers, so we must ensure here that the
// destination size is correct.
- MOZ_RELEASE_ASSERT(typeDef->arrayType().elementType_.size() ==
+ MOZ_RELEASE_ASSERT(typeDef->arrayType().elementType().size() ==
sizeof(AnyRef));
Rooted<WasmArrayObject*> arrayObj(
@@ -1771,7 +1771,7 @@ static bool ArrayCopyFromElem(JSContext* cx, Handle<WasmArrayObject*> arrayObj,
// Any data coming from an element segment will be an AnyRef. Writes into
// array memory are done with raw pointers, so we must ensure here that the
// destination size is correct.
- MOZ_RELEASE_ASSERT(typeDef->arrayType().elementType_.size() ==
+ MOZ_RELEASE_ASSERT(typeDef->arrayType().elementType().size() ==
sizeof(AnyRef));
// Get hold of the array.
@@ -1969,8 +1969,8 @@ static WasmArrayObject* UncheckedCastToArrayI16(HandleAnyRef ref) {
JSObject& object = ref.toJSObject();
WasmArrayObject& array = object.as<WasmArrayObject>();
DebugOnly<const ArrayType*> type(&array.typeDef().arrayType());
- MOZ_ASSERT(type->elementType_ == StorageType::I16);
- MOZ_ASSERT(type->isMutable_ == isMutable);
+ MOZ_ASSERT(type->elementType() == StorageType::I16);
+ MOZ_ASSERT(type->isMutable() == isMutable);
return &array;
}
@@ -2388,7 +2388,7 @@ bool Instance::init(JSContext* cx, const JSObjectVector& funcImports,
// StructLayout::close ensures this is an integral number of words.
MOZ_ASSERT((typeDefData->structTypeSize % sizeof(uintptr_t)) == 0);
} else {
- uint32_t arrayElemSize = typeDef.arrayType().elementType_.size();
+ uint32_t arrayElemSize = typeDef.arrayType().elementType().size();
typeDefData->arrayElemSize = arrayElemSize;
MOZ_ASSERT(arrayElemSize == 16 || arrayElemSize == 8 ||
arrayElemSize == 4 || arrayElemSize == 2 ||
diff --git a/js/src/wasm/WasmIonCompile.cpp b/js/src/wasm/WasmIonCompile.cpp
index 8310ef57e66..9754a778201 100644
--- a/js/src/wasm/WasmIonCompile.cpp
+++ b/js/src/wasm/WasmIonCompile.cpp
@@ -4464,11 +4464,11 @@ class FunctionCompiler {
// WasmStructObject, a MIR pointer to a value, and a field descriptor,
... (file diff truncated)