Firefox · SpiderMonkey
CVE-2026-4723
UAF in SpiderMonkey
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifjs/loader/ImportMap.cpp |
modified | |
MOZ_RAIIjs/public/Exception.h |
modified |
Files Changed
dom/base/nsContentUtils.cppdom/file/FileReader.cppdom/ipc/ClonedErrorHolder.cppjs/loader/ImportMap.cppjs/public/ErrorReport.hjs/public/Exception.hjs/src/jit-test/tests/errors/nuke-error-wrapper.jsjs/src/jsapi-tests/testFrontendErrors.cppjs/src/jsapi-tests/testWeakMap.cppjs/src/jsapi.cppjs/src/vm/ErrorObject.cppjs/src/vm/ErrorObject.hjs/xpconnect/src/XPCComponents.cppjs/xpconnect/src/XPCConvert.cpp
Patch
diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp
index be53d54487d..35f77abd536 100644
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -12097,11 +12097,12 @@ void nsContentUtils::ExtractErrorValues(
// Try to process as an Error object. Use the file/line/column values
// from the Error as they will be more specific to the root cause of
// the problem.
- if (JSErrorReport* err = JS_ErrorFromException(aCx, obj)) {
+ JS::BorrowedErrorReport err(aCx);
+ if (JS_ErrorFromException(aCx, obj, err)) {
// Use xpc to extract the error message only. We don't actually send
// this report anywhere.
RefPtr<xpc::ErrorReport> report = new xpc::ErrorReport();
- report->Init(err,
+ report->Init(err.get(),
nullptr, // toString result
false, // chrome
0); // window ID
diff --git a/dom/file/FileReader.cpp b/dom/file/FileReader.cpp
index bbe07d065b8..f2e962ca4ba 100644
--- a/dom/file/FileReader.cpp
+++ b/dom/file/FileReader.cpp
@@ -224,8 +224,8 @@ void FileReader::OnLoadEndArrayBuffer() {
JS_ClearPendingException(jsapi.cx());
JS::Rooted<JSObject*> exceptionObject(cx, &exceptionValue.toObject());
- JSErrorReport* er = JS_ErrorFromException(cx, exceptionObject);
- if (!er || er->message()) {
+ JS::BorrowedErrorReport er(cx);
+ if (!JS_ErrorFromException(cx, exceptionObject, er) || er->message()) {
FreeDataAndDispatchError(NS_ERROR_OUT_OF_MEMORY);
return;
}
diff --git a/dom/ipc/ClonedErrorHolder.cpp b/dom/ipc/ClonedErrorHolder.cpp
index 72c8bfbaf7f..54b37302e73 100644
--- a/dom/ipc/ClonedErrorHolder.cpp
+++ b/dom/ipc/ClonedErrorHolder.cpp
@@ -50,7 +50,8 @@ void ClonedErrorHolder::Init(JSContext* aCx, JS::Handle<JSObject*> aError,
ErrorResult& aRv) {
JS::Rooted<JSObject*> stack(aCx);
- if (JSErrorReport* err = JS_ErrorFromException(aCx, aError)) {
+ JS::BorrowedErrorReport err(aCx);
+ if (JS_ErrorFromException(aCx, aError, err)) {
mType = Type::JSError;
if (err->message()) {
mMessage = err->message().c_str();
@@ -309,7 +310,8 @@ bool ClonedErrorHolder::ToErrorValue(JSContext* aCx,
if (!mSourceLine.IsVoid()) {
JS::Rooted<JSObject*> errObj(aCx, &aResult.toObject());
- if (JSErrorReport* err = JS_ErrorFromException(aCx, errObj)) {
+ JS::BorrowedErrorReport err(aCx);
+ if (JS_ErrorFromException(aCx, errObj, err)) {
NS_ConvertUTF8toUTF16 sourceLine(mSourceLine);
// Because this string ends up being consumed as an nsDependentString
// in nsXPCComponents_Utils::ReportError, this needs to be a null
@@ -320,8 +322,8 @@ bool ClonedErrorHolder::ToErrorValue(JSContext* aCx,
// Corrupt data, leave linebuf unset.
} else if (JS::UniqueTwoByteChars buffer =
ToNullTerminatedJSStringBuffer(aCx, sourceLine)) {
- err->initOwnedLinebuf(buffer.release(), sourceLine.Length(),
- mTokenOffset);
+ err.get()->initOwnedLinebuf(buffer.release(), sourceLine.Length(),
+ mTokenOffset);
} else {
// Just ignore OOM and continue if the string copy failed.
JS_ClearPendingException(aCx);
diff --git a/js/loader/ImportMap.cpp b/js/loader/ImportMap.cpp
index a5b6a213875..6a11e1365dd 100644
--- a/js/loader/ImportMap.cpp
+++ b/js/loader/ImportMap.cpp
@@ -408,7 +408,8 @@ UniquePtr<ImportMap> ImportMap::ParseString(
}
MOZ_ASSERT(exn.isObject());
Rooted<JSObject*> obj(aCx, &exn.toObject());
- JSErrorReport* err = JS_ErrorFromException(aCx, obj);
+ JS::BorrowedErrorReport err(aCx);
+ MOZ_ALWAYS_TRUE(JS_ErrorFromException(aCx, obj, err));
if (err->exnType == JSEXN_SYNTAXERR) {
JS_ClearPendingException(aCx);
JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr,
diff --git a/js/public/ErrorReport.h b/js/public/ErrorReport.h
index 18852c24550..6b711f051dd 100644
--- a/js/public/ErrorReport.h
+++ b/js/public/ErrorReport.h
@@ -32,6 +32,7 @@
#include "js/AllocPolicy.h"
#include "js/CharacterEncoding.h" // JS::ConstUTF8CharsZ
#include "js/ColumnNumber.h" // JS::ColumnNumberOneOrigin
+#include "js/Exception.h" // JS::BorrowedErrorReport
#include "js/RootingAPI.h" // JS::HandleObject, JS::RootedObject
#include "js/UniquePtr.h" // js::UniquePtr
#include "js/Value.h" // JS::Value
@@ -414,14 +415,14 @@ struct MOZ_STACK_CLASS JS_PUBLIC_API ErrorReportBuilder {
JSString* maybeCreateReportFromDOMException(JS::HandleObject obj,
JSContext* cx);
- // We may have a provided JSErrorReport, so need a way to represent that.
+ // If non-nullptr, this is either |&ownedReport| or |borrowedReport.report_|.
JSErrorReport* reportp;
// Or we may need to synthesize a JSErrorReport one of our own.
JSErrorReport ownedReport;
- // Root our exception value to keep a possibly borrowed |reportp| alive.
- JS::RootedObject exnObject;
+ // Used to keep a possibly borrowed |reportp| alive.
+ JS::BorrowedErrorReport borrowedReport;
// And for our filename.
JS::UniqueChars filename;
diff --git a/js/public/Exception.h b/js/public/Exception.h
index a2e0c146136..4f62cd380aa 100644
--- a/js/public/Exception.h
+++ b/js/public/Exception.h
@@ -27,6 +27,37 @@ enum class ExceptionStackBehavior : bool {
// retrieved by JS::GetPendingExceptionStack.
Capture
};
+
+// Represents a |JSErrorReport*| borrowed from an ErrorObject. The object root
+// ensures the error report won't be freed in the scope of this class.
+//
+// Typical usage:
+//
+// BorrowedErrorReport report(cx);
+// if (JS_ErrorFromException(cx, obj, report)) {
+// // ... Use report->exnType, report.get(), etc.
+// }
+class MOZ_RAII BorrowedErrorReport {
+ Rooted<JSObject*> owner_;
+ JSErrorReport* report_ = nullptr;
+
+ public:
+ explicit BorrowedErrorReport(JSContext* cx) : owner_(cx) {}
+
+ void init(JSObject* owner, JSErrorReport* report) {
+ MOZ_ASSERT(owner);
+ MOZ_ASSERT(report);
+ owner_ = owner;
+ report_ = report;
+ }
+
+ JSErrorReport* get() const {
+ MOZ_ASSERT(report_);
+ return report_;
+ }
+ const JSErrorReport* operator->() const { return get(); }
+};
+
} // namespace JS
extern JS_PUBLIC_API bool JS_IsExceptionPending(JSContext* cx);
@@ -78,13 +109,13 @@ extern JS_PUBLIC_API void JS_ClearPendingException(JSContext* cx);
/**
* If the given object is an exception object, the exception will have (or be
- * able to lazily create) an error report struct, and this function will return
- * the address of that struct. Otherwise, it returns nullptr. The lifetime
- * of the error report struct that might be returned is the same as the
- * lifetime of the exception object.
+ * able to lazily create) an error report struct, and this function will
+ * populate |errorReport| with it and return true. Otherwise, returns false.
+ *
+ * See |BorrowedErrorReport| for a usage example.
*/
-extern JS_PUBLIC_API JSErrorReport* JS_ErrorFromException(JSContext* cx,
- JS::HandleObject obj);
+extern JS_PUBLIC_API bool JS_ErrorFromException(
+ JSContext* cx, JS::HandleObject obj, JS::BorrowedErrorReport& errorReport);
namespace JS {
diff --git a/js/src/jit-test/tests/errors/nuke-error-wrapper.js b/js/src/jit-test/tests/errors/nuke-error-wrapper.js
new file mode 100644
index 00000000000..0250a1e748b
--- /dev/null
+++ b/js/src/jit-test/tests/errors/nuke-error-wrapper.js
@@ -0,0 +1,35 @@
+// |jit-test| error:finished
+var g = newGlobal({newCompartment: true});
+g.evaluate(`
+ // Override Error.prototype.name with a getter that nukes CCWs
+ Object.defineProperty(Error.prototype, 'name', {
+ get: function() {
+ // Nuke all cross-compartment wrappers pointing into this realm.
+ // This makes the CCW in the main compartment (that roots our ErrorObject)
+ // become a dead proxy, removing the only reference to our ErrorObject.
+ nukeAllCCWs();
+
+ // Force a full GC to tenure the ErrorObject and compact heap.
+ // ErrorObject survives this GC because it's 'this' (on the C++ stack).
+ gc();
+
+ // Set maxBytes to current gcBytes so the NEXT allocation triggers GC.
+ // The next GC will collect the ErrorObject, freeing its JSErrorReport.
+ gcparam('maxBytes', gcparam('gcBytes'));
+
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/js/src/jit-test/tests/errors/nuke-error-wrapper.js b/js/src/jit-test/tests/errors/nuke-error-wrapper.js
new file mode 100644
index 00000000000..0250a1e748b
--- /dev/null
+++ b/js/src/jit-test/tests/errors/nuke-error-wrapper.js
@@ -0,0 +1,35 @@
+// |jit-test| error:finished
+var g = newGlobal({newCompartment: true});
+g.evaluate(`
+ // Override Error.prototype.name with a getter that nukes CCWs
+ Object.defineProperty(Error.prototype, 'name', {
+ get: function() {
+ // Nuke all cross-compartment wrappers pointing into this realm.
+ // This makes the CCW in the main compartment (that roots our ErrorObject)
+ // become a dead proxy, removing the only reference to our ErrorObject.
+ nukeAllCCWs();
+
+ // Force a full GC to tenure the ErrorObject and compact heap.
+ // ErrorObject survives this GC because it's 'this' (on the C++ stack).
+ gc();
+
+ // Set maxBytes to current gcBytes so the NEXT allocation triggers GC.
+ // The next GC will collect the ErrorObject, freeing its JSErrorReport.
+ gcparam('maxBytes', gcparam('gcBytes'));
+
+ // Return undefined (not a string) to force fallback to reportp->exnType
+ // and then reportp->newMessageString (both UAF after GC collects ErrorObject)
+ return undefined;
+ }
+ });
+
+ // Create the Error object in this compartment
+ this.err = new Error("finished");
+`);
+
+// Get a CCW (cross-compartment wrapper) to the ErrorObject.
+// Then clear the reference in compartment A so the only reference is through our CCW.
+// Then throw the foreign error at top level.
+var foreignError = g.err;
+g.err = null;
+throw foreignError;
Loading diff…
References
On This Page