Bug 1771084 part 2 - Add Realm option to freeze builtins. r=tcampbell,nika a=pascalc
authorJan de Mooij <jdemooij@mozilla.com>
Tue, 07 Jun 2022 15:40:44 +0000
changeset 689941 82e3067ad2e7a7ac3d93cea239d0ca7dc551a299
parent 689940 8269dcf3cfbf047ff516260a51442cb0d847929f
child 689942 6e4639dc3614364a537bfd20936c2305b67f0389
push id16755
push userpchevrel@mozilla.com
push dateSat, 11 Jun 2022 07:38:36 +0000
treeherdermozilla-beta@6e4639dc3614 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstcampbell, nika, pascalc
bugs1771084
milestone102.0
Bug 1771084 part 2 - Add Realm option to freeze builtins. r=tcampbell,nika a=pascalc Differential Revision: https://phabricator.services.mozilla.com/D147281
js/public/Realm.h
js/public/RealmOptions.h
js/src/jit-test/tests/basic/freeze-builtins.js
js/src/shell/js.cpp
js/src/vm/GlobalObject.cpp
js/src/vm/Realm.cpp
--- a/js/public/Realm.h
+++ b/js/public/Realm.h
@@ -85,16 +85,22 @@ extern JS_PUBLIC_API void SetRealmNameCa
 // GC, between collecting the global object and destroying the Realm.
 extern JS_PUBLIC_API JSObject* GetRealmGlobalOrNull(Realm* realm);
 
 // Initialize standard JS class constructors, prototypes, and any top-level
 // functions and constants associated with the standard classes (e.g. isNaN
 // for Number).
 extern JS_PUBLIC_API bool InitRealmStandardClasses(JSContext* cx);
 
+// If the current realm has the non-standard freezeBuiltins option set to true,
+// freeze the constructor object and seal the prototype.
+extern JS_PUBLIC_API bool MaybeFreezeCtorAndPrototype(JSContext* cx,
+                                                      HandleObject ctor,
+                                                      HandleObject maybeProto);
+
 /*
  * Ways to get various per-Realm objects. All the getters declared below operate
  * on the JSContext's current Realm.
  */
 
 extern JS_PUBLIC_API JSObject* GetRealmObjectPrototype(JSContext* cx);
 
 extern JS_PUBLIC_API JSObject* GetRealmFunctionPrototype(JSContext* cx);
--- a/js/public/RealmOptions.h
+++ b/js/public/RealmOptions.h
@@ -229,16 +229,25 @@ class JS_PUBLIC_API RealmCreationOptions
   // https://w3c.github.io/webappsec-secure-contexts/
   // https://bugzilla.mozilla.org/show_bug.cgi?id=1162772#c34
   bool secureContext() const { return secureContext_; }
   RealmCreationOptions& setSecureContext(bool flag) {
     secureContext_ = flag;
     return *this;
   }
 
+  // Non-standard option to freeze certain builtin constructors and seal their
+  // prototypes. Also defines these constructors on the global as non-writable
+  // and non-configurable.
+  bool freezeBuiltins() const { return freezeBuiltins_; }
+  RealmCreationOptions& setFreezeBuiltins(bool flag) {
+    freezeBuiltins_ = flag;
+    return *this;
+  }
+
   uint64_t profilerRealmID() const { return profilerRealmID_; }
   RealmCreationOptions& setProfilerRealmID(uint64_t id) {
     profilerRealmID_ = id;
     return *this;
   }
 
  private:
   JSTraceOp traceGlobal_ = nullptr;
@@ -260,16 +269,17 @@ class JS_PUBLIC_API RealmCreationOptions
   bool iteratorHelpers_ = false;
 #ifdef NIGHTLY_BUILD
   bool arrayGrouping_ = true;
 #endif
 #ifdef ENABLE_NEW_SET_METHODS
   bool newSetMethods_ = false;
 #endif
   bool secureContext_ = false;
+  bool freezeBuiltins_ = false;
 };
 
 /**
  * RealmBehaviors specifies behaviors of a realm that can be changed after the
  * realm's been created.
  */
 class JS_PUBLIC_API RealmBehaviors {
  public:
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/basic/freeze-builtins.js
@@ -0,0 +1,21 @@
+var g = newGlobal({freezeBuiltins: true});
+
+g.evaluate("" + function checkFrozen(name) {
+  // Check constructor on the global is non-writable/non-configurable.
+  let desc = Object.getOwnPropertyDescriptor(this, name);
+  assertEq(desc.writable, false);
+  assertEq(desc.configurable, false);
+
+  // Constructor must be frozen.
+  let ctor = desc.value;
+  assertEq(Object.isFrozen(ctor), true);
+
+  // Prototype must be sealed.
+  if (ctor.prototype) {
+    assertEq(Object.isSealed(ctor.prototype), true);
+  }
+});
+
+g.checkFrozen("Object");
+g.checkFrozen("Array");
+g.checkFrozen("Function");
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -7025,16 +7025,23 @@ static bool NewGlobal(JSContext* cx, uns
 
     if (!JS_GetProperty(cx, opts, "enableCoopAndCoep", &v)) {
       return false;
     }
     if (v.isBoolean()) {
       creationOptions.setCoopAndCoepEnabled(v.toBoolean());
     }
 
+    if (!JS_GetProperty(cx, opts, "freezeBuiltins", &v)) {
+      return false;
+    }
+    if (v.isBoolean()) {
+      creationOptions.setFreezeBuiltins(v.toBoolean());
+    }
+
     // On the web, the SharedArrayBuffer constructor is not installed as a
     // global property in pages that aren't isolated in a separate process (and
     // thus can't allow the structured cloning of shared memory).  Specify false
     // for this option to reproduce this behavior.
     if (!JS_GetProperty(cx, opts, "defineSharedArrayBufferConstructor", &v)) {
       return false;
     }
     if (v.isBoolean()) {
@@ -9503,16 +9510,19 @@ static const JSFunctionSpecWithHelp shel
 "      newCompartment: If true, the global will always be created in a new\n"
 "         compartment and zone.\n"
 "      invisibleToDebugger: If true, the global will be invisible to the\n"
 "         debugger (default false)\n"
 "      discardSource: If true, discard source after compiling a script\n"
 "         (default false).\n"
 "      useWindowProxy: the global will be created with a WindowProxy attached. In this\n"
 "          case, the WindowProxy will be returned.\n"
+"      freezeBuiltins: certain builtin constructors will be frozen when created and\n"
+"          their prototypes will be sealed. These constructors will be defined on the\n"
+"          global as non-configurable and non-writable.\n"
 "      immutablePrototype: whether the global's prototype is immutable.\n"
 "      principal: if present, its value converted to a number must be an\n"
 "         integer that fits in 32 bits; use that as the new realm's\n"
 "         principal. Shell principals are toys, meant only for testing; one\n"
 "         shell principal subsumes another if its set bits are a superset of\n"
 "         the other's. Thus, a principal of 0 subsumes nothing, while a\n"
 "         principals of ~0 subsumes all other principals. The absence of a\n"
 "         principal is treated as if its bits were 0xffff, for subsumption\n"
--- a/js/src/vm/GlobalObject.cpp
+++ b/js/src/vm/GlobalObject.cpp
@@ -209,16 +209,37 @@ bool GlobalObject::skipDeselectedConstru
     case JSProto_AsyncIterator:
       return !cx->realm()->creationOptions().getIteratorHelpersEnabled();
 
     default:
       MOZ_CRASH("unexpected JSProtoKey");
   }
 }
 
+static bool ShouldFreezeBuiltin(JSProtoKey key) {
+  switch (key) {
+    case JSProto_Object:
+    case JSProto_Array:
+    case JSProto_Function:
+      return true;
+    default:
+      return false;
+  }
+}
+
+static unsigned GetAttrsForResolvedGlobal(GlobalObject* global,
+                                          JSProtoKey key) {
+  unsigned attrs = JSPROP_RESOLVING;
+  if (global->realm()->creationOptions().freezeBuiltins() &&
+      ShouldFreezeBuiltin(key)) {
+    attrs |= JSPROP_PERMANENT | JSPROP_READONLY;
+  }
+  return attrs;
+}
+
 /* static*/
 bool GlobalObject::resolveConstructor(JSContext* cx,
                                       Handle<GlobalObject*> global,
                                       JSProtoKey key, IfClassIsDisabled mode) {
   MOZ_ASSERT(key != JSProto_Null);
   MOZ_ASSERT(!global->isStandardClassResolved(key));
   MOZ_ASSERT(cx->compartment() == global->compartment());
 
@@ -321,17 +342,18 @@ bool GlobalObject::resolveConstructor(JS
   if (!ctor) {
     return false;
   }
 
   RootedId id(cx, NameToId(ClassName(key, cx)));
   if (isObjectOrFunction) {
     if (clasp->specShouldDefineConstructor()) {
       RootedValue ctorValue(cx, ObjectValue(*ctor));
-      if (!DefineDataProperty(cx, global, id, ctorValue, JSPROP_RESOLVING)) {
+      unsigned attrs = GetAttrsForResolvedGlobal(global, key);
+      if (!DefineDataProperty(cx, global, id, ctorValue, attrs)) {
         return false;
       }
     }
 
     global->setConstructor(key, ctor);
   }
 
   if (const JSFunctionSpec* funs = clasp->specPrototypeFunctions()) {
@@ -362,16 +384,22 @@ bool GlobalObject::resolveConstructor(JS
 
   // Call the post-initialization hook, if provided.
   if (FinishClassInitOp finishInit = clasp->specFinishInitHook()) {
     if (!finishInit(cx, ctor, proto)) {
       return false;
     }
   }
 
+  if (ShouldFreezeBuiltin(key)) {
+    if (!JS::MaybeFreezeCtorAndPrototype(cx, ctor, proto)) {
+      return false;
+    }
+  }
+
   if (!isObjectOrFunction) {
     // Any operations that modifies the global object should be placed
     // after any other fallible operations.
 
     // Fallible operation that modifies the global object.
     if (clasp->specShouldDefineConstructor()) {
       bool shouldReallyDefine = true;
 
@@ -387,17 +415,18 @@ bool GlobalObject::resolveConstructor(JS
                    "shouldn't be defining SharedArrayBuffer if shared memory "
                    "is disabled");
 
         shouldReallyDefine = options.defineSharedArrayBufferConstructor();
       }
 
       if (shouldReallyDefine) {
         RootedValue ctorValue(cx, ObjectValue(*ctor));
-        if (!DefineDataProperty(cx, global, id, ctorValue, JSPROP_RESOLVING)) {
+        unsigned attrs = GetAttrsForResolvedGlobal(global, key);
+        if (!DefineDataProperty(cx, global, id, ctorValue, attrs)) {
           return false;
         }
       }
     }
 
     // Infallible operations that modify the global object.
     global->setConstructor(key, ctor);
     if (proto) {
--- a/js/src/vm/Realm.cpp
+++ b/js/src/vm/Realm.cpp
@@ -695,16 +695,33 @@ JS_PUBLIC_API JSObject* JS::GetRealmGlob
 
 JS_PUBLIC_API bool JS::InitRealmStandardClasses(JSContext* cx) {
   MOZ_ASSERT(!cx->zone()->isAtomsZone());
   AssertHeapIsIdle();
   CHECK_THREAD(cx);
   return GlobalObject::initStandardClasses(cx, cx->global());
 }
 
+JS_PUBLIC_API bool JS::MaybeFreezeCtorAndPrototype(JSContext* cx,
+                                                   HandleObject ctor,
+                                                   HandleObject maybeProto) {
+  if (MOZ_LIKELY(!cx->realm()->creationOptions().freezeBuiltins())) {
+    return true;
+  }
+  if (!SetIntegrityLevel(cx, ctor, IntegrityLevel::Frozen)) {
+    return false;
+  }
+  if (maybeProto) {
+    if (!SetIntegrityLevel(cx, maybeProto, IntegrityLevel::Sealed)) {
+      return false;
+    }
+  }
+  return true;
+}
+
 JS_PUBLIC_API JSObject* JS::GetRealmObjectPrototype(JSContext* cx) {
   CHECK_THREAD(cx);
   return GlobalObject::getOrCreateObjectPrototype(cx, cx->global());
 }
 
 JS_PUBLIC_API JSObject* JS::GetRealmFunctionPrototype(JSContext* cx) {
   CHECK_THREAD(cx);
   return GlobalObject::getOrCreateFunctionPrototype(cx, cx->global());