Skip to content

Commit

Permalink
Object.prototype should be an immutable prototype exotic object.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dilijev committed Apr 27, 2016
1 parent 3efff16 commit d917afa
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 33 deletions.
2 changes: 2 additions & 0 deletions lib/Parser/rterrors.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_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 */
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)
Expand Down
10 changes: 10 additions & 0 deletions lib/Runtime/Library/JavascriptObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,16 @@ namespace Js
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
// a. Let p be V.
// b. Repeat, while p is not null
Expand Down
1 change: 1 addition & 0 deletions lib/Runtime/Library/ObjectPrototypeObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ namespace Js
// 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].
bool is__proto__Enabled() const { return __proto__Enabled; }
BOOL IsProtoImmutable() const { return true; }

void PostDefineOwnProperty__proto__(RecyclableObject* obj);
};
Expand Down
1 change: 1 addition & 0 deletions lib/Runtime/Types/RecyclableObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ namespace Js {
virtual BOOL IsConfigurable(PropertyId propertyId) { return false; }
virtual BOOL IsEnumerable(PropertyId propertyId) { 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 void ThrowIfCannotDefineProperty(PropertyId propId, PropertyDescriptor descriptor);
virtual void ThrowIfCannotGetOwnPropertyDescriptor(PropertyId propId) {}
Expand Down
6 changes: 5 additions & 1 deletion test/UnitTestFramework/UnitTestFramework.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,11 +294,15 @@ var assert = function assert() {
///
/// expectedException: A specific exception type, e.g. TypeError.
/// 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/>
/// 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("{"); }, SyntaxError, "expected SyntaxError with message about expected semicolon.", "Expected ';'")
/// </summary>
var noException = {}; // Some unique object which will not be equal to anything else.
var exception = noException; // Set default value.
Expand Down
13 changes: 5 additions & 8 deletions test/es6/proto_disable.baseline
Original file line number Diff line number Diff line change
@@ -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.freeze(Object.prototype)
PASSED
*** Running test #3 (2): delete Object.prototype.__proto__
*** Running test #2 (1): delete Object.prototype.__proto__
delete Object.prototype.__proto__
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__", {enumerable: false})
Object.defineProperty(Object.prototype, "__proto__", {configurable: true})
Expand All @@ -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__", {set: function () { return "custom setter" }, enumerable: false, configurable: true})
PASSED
*** Running test #5 (4): Change Object.prototype.__proto__ getter or setter
*** Running test #4 (3): Change Object.prototype.__proto__ getter or setter
PASSED
Summary of tests: total executed: 5; passed: 5; failed: 0
Summary of tests: total executed: 4; passed: 4; failed: 0
24 changes: 0 additions & 24 deletions test/es6/proto_disable.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,6 @@ if (this.WScript && this.WScript.LoadScriptFile) {
}

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",
body: function () {
Expand Down
45 changes: 45 additions & 0 deletions test/es7/immutable-prototype.js
Original file line number Diff line number Diff line change
@@ -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 changes: 6 additions & 0 deletions test/es7/rlexe.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,10 @@
<tags>BugFix</tags>
</default>
</test>
<test>
<default>
<files>immutable-prototype.js</files>
<compile-flags>-args summary -endargs</compile-flags>
</default>
</test>
</regress-exe>

0 comments on commit d917afa

Please sign in to comment.