Bladeren bron

Object.prototype should be an immutable prototype exotic object.

This means that any attempts to set the [[Prototype]] slot of the Object Prototype Object (the object which is initially pointed to by Object.prototype) notably by using Object.prototype.__proto__ or Object.setPrototypeOf(Object.prototype, otherObject) should fail with a TypeError as per spec.

This is the only exotic behavior of this object.

I chose to implement this by adding the virtual method IsProtoImmutable to RecyclableObject with the default return value false, and to override this function in ObjectPrototypeObject to return true. This allows for a future implementation of Object.setImmutablePrototype which would allow other objects to exhibit this behavior. A flag could be added which can be set, and this method would return the value of that flag. In that case, the implementation in ObjectPrototypeObject should remain hardcoded to false to avoid accidentally allowing the value if IsProtoImmutable to change.

Removed defunct test.

See 19.1.3:
The Object prototype object is the intrinsic object %ObjectPrototype%.
The Object prototype object is an immutable prototype exotic object.
See 9.4.7:
An immutable prototype exotic object is an exotic object that has an immutable [[Prototype]] internal slot.

Fixes #261

Removed redundant and defunct tests, fixed error description.
Doug Ilijev 10 jaren geleden
bovenliggende
commit
d917afa76c

+ 2 - 0
lib/Parser/rterrors.h

@@ -275,6 +275,8 @@ RT_ERROR_MSG(JSERR_DeletePropertyWithSuper, 5146, "Unable to delete property '%s
 RT_ERROR_MSG(JSERR_DetachedTypedArray, 5147, "%s: The ArrayBuffer is detached.", "The ArrayBuffer is detached.", kjstTypeError, 0)
 RT_ERROR_MSG(JSERR_DetachedTypedArray, 5147, "%s: The ArrayBuffer is detached.", "The ArrayBuffer is detached.", kjstTypeError, 0)
 RT_ERROR_MSG(JSERR_AsmJsCompileError, 5148, "%s: Compiling asm.js failed.", "Compiling asm.js failed.", kjstError, 0)
 RT_ERROR_MSG(JSERR_AsmJsCompileError, 5148, "%s: Compiling asm.js failed.", "Compiling asm.js failed.", kjstError, 0)
 
 
+RT_ERROR_MSG(JSERR_ImmutablePrototypeSlot, 5149, "%s: Can't set the prototype of this object.", "Can't set the prototype of this object.", kjstTypeError, 0)
+
 /* Error messages for misbehaved Async Operations for use in Promise.js */
 /* Error messages for misbehaved Async Operations for use in Promise.js */
 RT_ERROR_MSG(ASYNCERR_NoErrorInErrorState, 5200, "", "Status is 'error', but getResults did not return an error", kjstError, 0)
 RT_ERROR_MSG(ASYNCERR_NoErrorInErrorState, 5200, "", "Status is 'error', but getResults did not return an error", kjstError, 0)
 RT_ERROR_MSG(ASYNCERR_InvalidStatusArg, 5201, "", "Missing or invalid status parameter passed to completed handler", kjstError, 0)
 RT_ERROR_MSG(ASYNCERR_InvalidStatusArg, 5201, "", "Missing or invalid status parameter passed to completed handler", kjstError, 0)

+ 10 - 0
lib/Runtime/Library/JavascriptObject.cpp

@@ -176,6 +176,16 @@ namespace Js
             return FALSE;
             return FALSE;
         }
         }
 
 
+        if (object->IsProtoImmutable())
+        {
+            // ES2016 19.1.3:
+            // The Object prototype object is the intrinsic object %ObjectPrototype%.
+            // The Object prototype object is an immutable prototype exotic object.
+            // ES2016 9.4.7:
+            // An immutable prototype exotic object is an exotic object that has an immutable [[Prototype]] internal slot.
+            JavascriptError::ThrowTypeError(scriptContext, JSERR_ImmutablePrototypeSlot);
+        }
+
         // 6.   If V is not null, then
         // 6.   If V is not null, then
         //  a.  Let p be V.
         //  a.  Let p be V.
         //  b.  Repeat, while p is not null
         //  b.  Repeat, while p is not null

+ 1 - 0
lib/Runtime/Library/ObjectPrototypeObject.h

@@ -36,6 +36,7 @@ namespace Js
         // Indicates if __proto__ is enabled currently (note that it can be disabled and re-enabled),
         // Indicates if __proto__ is enabled currently (note that it can be disabled and re-enabled),
         // only useful for diagnostics to decide displaying __proto__ or [prototype].
         // only useful for diagnostics to decide displaying __proto__ or [prototype].
         bool is__proto__Enabled() const { return __proto__Enabled; }
         bool is__proto__Enabled() const { return __proto__Enabled; }
+        BOOL IsProtoImmutable() const { return true; }
 
 
         void PostDefineOwnProperty__proto__(RecyclableObject* obj);
         void PostDefineOwnProperty__proto__(RecyclableObject* obj);
     };
     };

+ 1 - 0
lib/Runtime/Types/RecyclableObject.h

@@ -285,6 +285,7 @@ namespace Js {
         virtual BOOL IsConfigurable(PropertyId propertyId) { return false; }
         virtual BOOL IsConfigurable(PropertyId propertyId) { return false; }
         virtual BOOL IsEnumerable(PropertyId propertyId) { return false; }
         virtual BOOL IsEnumerable(PropertyId propertyId) { return false; }
         virtual BOOL IsExtensible() { return false; }
         virtual BOOL IsExtensible() { return false; }
+        virtual BOOL IsProtoImmutable() const { return false; }
         virtual BOOL PreventExtensions() { return false; };     // Sets [[Extensible]] flag of instance to false
         virtual BOOL PreventExtensions() { return false; };     // Sets [[Extensible]] flag of instance to false
         virtual void ThrowIfCannotDefineProperty(PropertyId propId, PropertyDescriptor descriptor);
         virtual void ThrowIfCannotDefineProperty(PropertyId propId, PropertyDescriptor descriptor);
         virtual void ThrowIfCannotGetOwnPropertyDescriptor(PropertyId propId) {}
         virtual void ThrowIfCannotGetOwnPropertyDescriptor(PropertyId propId) {}

+ 5 - 1
test/UnitTestFramework/UnitTestFramework.js

@@ -294,11 +294,15 @@ var assert = function assert() {
             ///
             ///
             /// expectedException:  A specific exception type, e.g. TypeError.
             /// expectedException:  A specific exception type, e.g. TypeError.
             ///                     if undefined, this will pass if testFunction throws any exception.<br/>
             ///                     if undefined, this will pass if testFunction throws any exception.<br/>
-            /// expectedErrorMessage: If specified, verifies the thrown Error has expected message.<br/><br/>
+            /// message: Test-provided explanation of why this particular exception should be thrown.<br/>
+            /// expectedErrorMessage: If specified, verifies the thrown Error has expected message.<br/>
+            ///                       You only need to specify this if you are looking for a specific error message.<br/>
+            ///                       Does not require the prefix of e.g. "TypeError:" that would be printed by the host.<br/><br/>
             ///
             ///
             /// Example:<br/>
             /// Example:<br/>
             /// assert.throws(function() { eval("{"); }, SyntaxError, "expected SyntaxError")<br/>
             /// assert.throws(function() { eval("{"); }, SyntaxError, "expected SyntaxError")<br/>
             /// assert.throws(function() { eval("{"); }) // -- use this when you don't care which exception is thrown.<br/>
             /// assert.throws(function() { eval("{"); }) // -- use this when you don't care which exception is thrown.<br/>
+            /// assert.throws(function() { eval("{"); }, SyntaxError, "expected SyntaxError with message about expected semicolon.", "Expected ';'")
             /// </summary>
             /// </summary>
             var noException = {};         // Some unique object which will not be equal to anything else.
             var noException = {};         // Some unique object which will not be equal to anything else.
             var exception = noException;  // Set default value.
             var exception = noException;  // Set default value.

+ 5 - 8
test/es6/proto_disable.baseline

@@ -1,14 +1,11 @@
-*** Running test #1 (0): Change Object.prototype.__proto__ value
-Object.prototype.__proto__ = null
-PASSED
-*** Running test #2 (1): seal/freeze Object.prototype
+*** Running test #1 (0): seal/freeze Object.prototype
 Object.seal(Object.prototype)
 Object.seal(Object.prototype)
 Object.freeze(Object.prototype)
 Object.freeze(Object.prototype)
 PASSED
 PASSED
-*** Running test #3 (2): delete Object.prototype.__proto__
+*** Running test #2 (1): delete Object.prototype.__proto__
 delete Object.prototype.__proto__
 delete Object.prototype.__proto__
 PASSED
 PASSED
-*** Running test #4 (3): DefineOwnProperty with missing/different attribute set
+*** Running test #3 (2): DefineOwnProperty with missing/different attribute set
 Object.defineProperty(Object.prototype, "__proto__", {})
 Object.defineProperty(Object.prototype, "__proto__", {})
 Object.defineProperty(Object.prototype, "__proto__", {enumerable: false})
 Object.defineProperty(Object.prototype, "__proto__", {enumerable: false})
 Object.defineProperty(Object.prototype, "__proto__", {configurable: true})
 Object.defineProperty(Object.prototype, "__proto__", {configurable: true})
@@ -18,6 +15,6 @@ Object.defineProperty(Object.prototype, "__proto__", {enumerable: false, configu
 Object.defineProperty(Object.prototype, "__proto__", {value: 234, writable: true, enumerable: false, configurable: true})
 Object.defineProperty(Object.prototype, "__proto__", {value: 234, writable: true, enumerable: false, configurable: true})
 Object.defineProperty(Object.prototype, "__proto__", {set: function () { return "custom setter" }, enumerable: false, configurable: true})
 Object.defineProperty(Object.prototype, "__proto__", {set: function () { return "custom setter" }, enumerable: false, configurable: true})
 PASSED
 PASSED
-*** Running test #5 (4): Change Object.prototype.__proto__ getter or setter
+*** Running test #4 (3): Change Object.prototype.__proto__ getter or setter
 PASSED
 PASSED
-Summary of tests: total executed: 5; passed: 5; failed: 0
+Summary of tests: total executed: 4; passed: 4; failed: 0

+ 0 - 24
test/es6/proto_disable.js

@@ -9,30 +9,6 @@ if (this.WScript && this.WScript.LoadScriptFile) {
 }
 }
 
 
 var tests = [
 var tests = [
-    {
-        name: "Change Object.prototype.__proto__ value",
-        body: function () {
-            // This considered no-op: Object.prototype.__proto__ = null
-            verify_disable("Object.prototype.__proto__ = null", KEEP_ENABLED);
-
-            // Set to these primitives will throw and make no change
-            [undefined, 0, 123, -12.3, NaN, Infinity, true, false, "str"].forEach(
-                function (newValue) {
-                    Object.prototype.__proto__ = newValue;
-                    verify__proto__enabled();
-                });
-
-            // Set to any objects will throw and make no change
-            [new Boolean(), new Number(12), new String("string object"), {}, [], Object.prototype, Math.sin, assert.throws].forEach(
-                function (newValue) {
-                    assert.throws__proto__Cyclic(function () {
-                        Object.prototype.__proto__ = newValue;
-                    });
-                    verify__proto__enabled();
-                });
-        }
-    },
-
     {
     {
         name: "seal/freeze Object.prototype",
         name: "seal/freeze Object.prototype",
         body: function () {
         body: function () {

+ 45 - 0
test/es7/immutable-prototype.js

@@ -0,0 +1,45 @@
+//-------------------------------------------------------------------------------------------------------
+// Copyright (C) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
+//-------------------------------------------------------------------------------------------------------
+
+// ES7 Object Prototype object has an immutable [[Prototype]] internal slot
+// See: 19.1.3 Properties of the Object Prototype Object
+// See: 9.4.7 Immutable Prototype Exotic Objects
+
+WScript.LoadScriptFile("..\\UnitTestFramework\\UnitTestFramework.js");
+
+var tests = [
+    {
+        name: "Not okay to set Object.prototype.[[Prototype]] using __proto__",
+        body: function () {
+            var objectPrototypeObject = Object.getPrototypeOf(Object.prototype)
+            var b = Object.create(null)
+
+            assert.throws(function () { Object.prototype.__proto__ = b },
+                TypeError,
+                "It should not be okay to set Object.prototype.[[Prototype]] using __proto__",
+                "Can't set the prototype of this object.")
+
+            assert.areEqual(objectPrototypeObject, Object.prototype.__proto__, "Object.prototype.__proto__ is unchanged")
+            assert.areEqual(objectPrototypeObject, Object.getPrototypeOf(Object.prototype), "Object.getPrototypeOf(Object.prototype) is unchanged")
+        }
+    },
+    {
+        name: "Not okay to set Object.prototype.[[Prototype]] using Object.setPrototypeOf",
+        body: function () {
+            var objectPrototypeObject = Object.getPrototypeOf(Object.prototype)
+            var b = Object.create(null)
+
+            assert.throws(function () { Object.setPrototypeOf(Object.prototype, b) },
+                TypeError,
+                "It should not be okay to set Object.prototype.[[Prototype]] using Object.setPrototypeOf",
+                "Can't set the prototype of this object.")
+
+            assert.areEqual(objectPrototypeObject, Object.prototype.__proto__, "Object.prototype.__proto__ is unchanged")
+            assert.areEqual(objectPrototypeObject, Object.getPrototypeOf(Object.prototype), "Object.getPrototypeOf(Object.prototype) is unchanged")
+        }
+    },
+];
+
+testRunner.runTests(tests, { verbose: WScript.Arguments[0] != "summary" });

+ 6 - 0
test/es7/rlexe.xml

@@ -59,4 +59,10 @@
       <tags>BugFix</tags>
       <tags>BugFix</tags>
     </default>
     </default>
   </test>
   </test>
+  <test>
+    <default>
+      <files>immutable-prototype.js</files>
+      <compile-flags>-args summary -endargs</compile-flags>
+    </default>
+  </test>
 </regress-exe>
 </regress-exe>