From 2191390bed78914883d4a7e4497d0948e8857d1d Mon Sep 17 00:00:00 2001 From: Tooru Fujisawa Date: Fri, 13 May 2022 22:02:40 +0000 Subject: [PATCH] Bug 1768060 - Add a wrapper for Cu.import return value that supports lexical variable. r=jonco,Standard8 This wrapper exposes all lexical variables in Cu.import return value, that allows us removing `this.foo = foo;` hack, in bug 1610653 patches, without affecting the not-in-tree consumer. Differential Revision: https://phabricator.services.mozilla.com/D145938 --- js/xpconnect/loader/JSMEnvironmentProxy.cpp | 260 +++++++++++++++++++ js/xpconnect/loader/JSMEnvironmentProxy.h | 31 +++ js/xpconnect/loader/moz.build | 1 + js/xpconnect/loader/mozJSComponentLoader.cpp | 50 +--- js/xpconnect/tests/unit/test_import_shim.js | 25 +- 5 files changed, 319 insertions(+), 48 deletions(-) create mode 100644 js/xpconnect/loader/JSMEnvironmentProxy.cpp create mode 100644 js/xpconnect/loader/JSMEnvironmentProxy.h diff --git a/js/xpconnect/loader/JSMEnvironmentProxy.cpp b/js/xpconnect/loader/JSMEnvironmentProxy.cpp new file mode 100644 index 000000000000..6cd41bb764e9 --- /dev/null +++ b/js/xpconnect/loader/JSMEnvironmentProxy.cpp @@ -0,0 +1,260 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "JSMEnvironmentProxy.h" + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/Maybe.h" // mozilla::Maybe + +#include // size_t + +#include "jsapi.h" // JS_HasExtensibleLexicalEnvironment, JS_ExtensibleLexicalEnvironment +#include "js/Class.h" // JS::ObjectOpResult +#include "js/ErrorReport.h" // JS_ReportOutOfMemory +#include "js/GCVector.h" // JS::RootedVector +#include "js/Id.h" // JS::PropertyKey +#include "js/PropertyAndElement.h" // JS::IdVector, JS_HasPropertyById, JS_HasOwnPropertyById, JS_GetPropertyById, JS_Enumerate +#include "js/PropertyDescriptor.h" // JS::PropertyDescriptor, JS_GetOwnPropertyDescriptorById +#include "js/PropertyDescriptor.h" // JS::PropertyDescriptor, JS_GetOwnPropertyDescriptorById +#include "js/Proxy.h" // js::ProxyOptions, js::NewProxyObject, js::GetProxyPrivate +#include "js/RootingAPI.h" // JS::Rooted, JS::Handle, JS::MutableHandle +#include "js/TypeDecls.h" // JSContext, JSObject, JS::MutableHandleVector +#include "js/Value.h" // JS::Value, JS::UndefinedValue, JS_UNINITIALIZED_LEXICAL +#include "js/friend/ErrorMessages.h" // JSMSG_* + +namespace mozilla { +namespace loader { + +struct JSMEnvironmentProxyHandler : public js::BaseProxyHandler { + JSMEnvironmentProxyHandler() : BaseProxyHandler(&gFamily, false) {} + + bool defineProperty(JSContext* aCx, JS::Handle aProxy, + JS::Handle aId, + JS::Handle aDesc, + JS::ObjectOpResult& aResult) const override { + return aResult.fail(JSMSG_CANT_DEFINE_PROP_OBJECT_NOT_EXTENSIBLE); + } + + bool getPrototype(JSContext* aCx, JS::Handle aProxy, + JS::MutableHandle aProtop) const override { + aProtop.set(nullptr); + return true; + } + + bool setPrototype(JSContext* aCx, JS::Handle aProxy, + JS::Handle aProto, + JS::ObjectOpResult& aResult) const override { + if (!aProto) { + return aResult.succeed(); + } + return aResult.failCantSetProto(); + } + + bool getPrototypeIfOrdinary( + JSContext* aCx, JS::Handle aProxy, bool* aIsOrdinary, + JS::MutableHandle aProtop) const override { + *aIsOrdinary = false; + return true; + } + + bool setImmutablePrototype(JSContext* aCx, JS::Handle aProxy, + bool* aSucceeded) const override { + *aSucceeded = true; + return true; + } + + bool preventExtensions(JSContext* aCx, JS::Handle aProxy, + JS::ObjectOpResult& aResult) const override { + aResult.succeed(); + return true; + } + + bool isExtensible(JSContext* aCx, JS::Handle aProxy, + bool* aExtensible) const override { + *aExtensible = false; + return true; + } + + bool set(JSContext* aCx, JS::Handle aProxy, + JS::Handle aId, JS::Handle aValue, + JS::Handle aReceiver, + JS::ObjectOpResult& aResult) const override { + return aResult.failReadOnly(); + } + + bool delete_(JSContext* aCx, JS::Handle aProxy, + JS::Handle aId, + JS::ObjectOpResult& aResult) const override { + return aResult.failCantDelete(); + } + + bool getOwnPropertyDescriptor( + JSContext* aCx, JS::Handle aProxy, + JS::Handle aId, + JS::MutableHandle> aDesc) + const override; + bool has(JSContext* aCx, JS::Handle aProxy, + JS::Handle aId, bool* aBp) const override; + bool get(JSContext* aCx, JS::Handle aProxy, + JS::Handle aReceiver, JS::Handle aId, + JS::MutableHandle aVp) const override; + bool ownPropertyKeys( + JSContext* aCx, JS::Handle aProxy, + JS::MutableHandleVector aProps) const override; + + private: + static JSObject* getGlobal(JSContext* aCx, JS::Handle aProxy) { + JS::Rooted globalObj(aCx, + &js::GetProxyPrivate(aProxy).toObject()); + return globalObj; + } + + public: + static const char gFamily; + static const JSMEnvironmentProxyHandler gHandler; +}; + +const JSMEnvironmentProxyHandler JSMEnvironmentProxyHandler::gHandler; +const char JSMEnvironmentProxyHandler::gFamily = 0; + +JSObject* ResolveModuleObjectPropertyById(JSContext* aCx, + JS::Handle aModObj, + JS::Handle aId) { + if (JS_HasExtensibleLexicalEnvironment(aModObj)) { + JS::Rooted lexical(aCx, + JS_ExtensibleLexicalEnvironment(aModObj)); + bool found; + if (!JS_HasOwnPropertyById(aCx, lexical, aId, &found)) { + return nullptr; + } + if (found) { + return lexical; + } + } + return aModObj; +} + +JSObject* ResolveModuleObjectProperty(JSContext* aCx, + JS::Handle aModObj, + const char* aName) { + if (JS_HasExtensibleLexicalEnvironment(aModObj)) { + RootedObject lexical(aCx, JS_ExtensibleLexicalEnvironment(aModObj)); + bool found; + if (!JS_HasOwnProperty(aCx, lexical, aName, &found)) { + return nullptr; + } + if (found) { + return lexical; + } + } + return aModObj; +} + +bool JSMEnvironmentProxyHandler::getOwnPropertyDescriptor( + JSContext* aCx, JS::Handle aProxy, + JS::Handle aId, + JS::MutableHandle> aDesc) const { + JS::Rooted globalObj(aCx, getGlobal(aCx, aProxy)); + JS::Rooted holder( + aCx, ResolveModuleObjectPropertyById(aCx, globalObj, aId)); + if (!JS_GetOwnPropertyDescriptorById(aCx, holder, aId, aDesc)) { + return false; + } + + if (aDesc.get().isNothing()) { + return true; + } + + JS::PropertyDescriptor& desc = *aDesc.get(); + + if (desc.hasValue()) { + if (desc.value().isMagic(JS_UNINITIALIZED_LEXICAL)) { + desc.setValue(JS::UndefinedValue()); + } + } + + desc.setConfigurable(false); + desc.setEnumerable(true); + if (!desc.isAccessorDescriptor()) { + desc.setWritable(false); + } + + return true; +} + +bool JSMEnvironmentProxyHandler::has(JSContext* aCx, + JS::Handle aProxy, + JS::Handle aId, + bool* aBp) const { + JS::Rooted globalObj(aCx, getGlobal(aCx, aProxy)); + JS::Rooted holder( + aCx, ResolveModuleObjectPropertyById(aCx, globalObj, aId)); + return JS_HasPropertyById(aCx, holder, aId, aBp); +} + +bool JSMEnvironmentProxyHandler::get(JSContext* aCx, + JS::Handle aProxy, + JS::Handle aReceiver, + JS::Handle aId, + JS::MutableHandle aVp) const { + JS::Rooted globalObj(aCx, getGlobal(aCx, aProxy)); + JS::Rooted holder( + aCx, ResolveModuleObjectPropertyById(aCx, globalObj, aId)); + if (!JS_GetPropertyById(aCx, holder, aId, aVp)) { + return false; + } + + if (aVp.isMagic(JS_UNINITIALIZED_LEXICAL)) { + aVp.setUndefined(); + } + + return true; +} + +bool JSMEnvironmentProxyHandler::ownPropertyKeys( + JSContext* aCx, JS::Handle aProxy, + JS::MutableHandleVector aProps) const { + JS::Rooted globalObj(aCx, getGlobal(aCx, aProxy)); + JS::Rooted globalIds(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, globalObj, &globalIds)) { + return false; + } + + for (size_t i = 0; i < globalIds.length(); i++) { + if (!aProps.append(globalIds[i])) { + JS_ReportOutOfMemory(aCx); + return false; + } + } + + JS::RootedObject lexicalEnv(aCx, JS_ExtensibleLexicalEnvironment(globalObj)); + JS::Rooted lexicalIds(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, lexicalEnv, &lexicalIds)) { + return false; + } + + for (size_t i = 0; i < lexicalIds.length(); i++) { + if (!aProps.append(lexicalIds[i])) { + JS_ReportOutOfMemory(aCx); + return false; + } + } + + return true; +} + +JSObject* CreateJSMEnvironmentProxy(JSContext* aCx, + JS::Handle aGlobalObj) { + js::ProxyOptions options; + options.setLazyProto(true); + + JS::Rooted globalVal(aCx, JS::ObjectValue(*aGlobalObj)); + return NewProxyObject(aCx, &JSMEnvironmentProxyHandler::gHandler, globalVal, + nullptr, options); +} + +} // namespace loader +} // namespace mozilla diff --git a/js/xpconnect/loader/JSMEnvironmentProxy.h b/js/xpconnect/loader/JSMEnvironmentProxy.h new file mode 100644 index 000000000000..f45d7e380156 --- /dev/null +++ b/js/xpconnect/loader/JSMEnvironmentProxy.h @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_loader_JSMEnvironmentProxy_h +#define mozilla_loader_JSMEnvironmentProxy_h + +#include "js/Id.h" // JS::PropertyKey +#include "js/TypeDecls.h" // JSContext, JSObject +#include "js/RootingAPI.h" // JS::Handle + +namespace mozilla { +namespace loader { + +JSObject* ResolveModuleObjectPropertyById(JSContext* aCx, + JS::Handle aModObj, + JS::Handle aId); + +JSObject* ResolveModuleObjectProperty(JSContext* aCx, + JS::Handle aModObj, + const char* aName); + +JSObject* CreateJSMEnvironmentProxy(JSContext* aCx, + JS::Handle aGlobalObj); + +} // namespace loader +} // namespace mozilla + +#endif // mozilla_loader_JSMEnvironmentProxy_h diff --git a/js/xpconnect/loader/moz.build b/js/xpconnect/loader/moz.build index 914ded9b0a8f..f2c693e2d63d 100644 --- a/js/xpconnect/loader/moz.build +++ b/js/xpconnect/loader/moz.build @@ -8,6 +8,7 @@ UNIFIED_SOURCES += [ "AutoMemMap.cpp", "ChromeScriptLoader.cpp", "ComponentModuleLoader.cpp", + "JSMEnvironmentProxy.cpp", "ModuleEnvironmentProxy.cpp", "mozJSLoaderUtils.cpp", "mozJSSubScriptLoader.cpp", diff --git a/js/xpconnect/loader/mozJSComponentLoader.cpp b/js/xpconnect/loader/mozJSComponentLoader.cpp index de37e338dad4..99aeb2fea6fc 100644 --- a/js/xpconnect/loader/mozJSComponentLoader.cpp +++ b/js/xpconnect/loader/mozJSComponentLoader.cpp @@ -49,6 +49,7 @@ #include "nsReadableUtils.h" #include "nsXULAppAPI.h" #include "WrapperFactory.h" +#include "JSMEnvironmentProxy.h" #include "ModuleEnvironmentProxy.h" #include "AutoMemMap.h" @@ -344,25 +345,6 @@ mozJSComponentLoader::~mozJSComponentLoader() { StaticRefPtr mozJSComponentLoader::sSelf; -// For terrible compatibility reasons, we need to consider both the global -// lexical environment and the global of modules when searching for exported -// symbols. -static JSObject* ResolveModuleObjectProperty(JSContext* aCx, - HandleObject aModObj, - const char* name) { - if (JS_HasExtensibleLexicalEnvironment(aModObj)) { - RootedObject lexical(aCx, JS_ExtensibleLexicalEnvironment(aModObj)); - bool found; - if (!JS_HasOwnProperty(aCx, lexical, name, &found)) { - return nullptr; - } - if (found) { - return lexical; - } - } - return aModObj; -} - const mozilla::Module* mozJSComponentLoader::LoadModule(FileLocation& aFile) { if (!NS_IsMainThread()) { MOZ_ASSERT(false, "Don't use JS components off the main thread"); @@ -1187,22 +1169,6 @@ nsresult mozJSComponentLoader::GetComponentLoadStack( #endif } -static JSObject* ResolveModuleObjectPropertyById(JSContext* aCx, - HandleObject aModObj, - HandleId id) { - if (JS_HasExtensibleLexicalEnvironment(aModObj)) { - RootedObject lexical(aCx, JS_ExtensibleLexicalEnvironment(aModObj)); - bool found; - if (!JS_HasOwnPropertyById(aCx, lexical, id, &found)) { - return nullptr; - } - if (found) { - return lexical; - } - } - return aModObj; -} - nsresult mozJSComponentLoader::ImportInto(const nsACString& aLocation, HandleObject targetObj, JSContext* cx, MutableHandleObject vp) { @@ -1454,7 +1420,19 @@ nsresult mozJSComponentLoader::Import(JSContext* aCx, } MOZ_ASSERT(mod->obj, "Import table contains entry with no object"); - aModuleGlobal.set(mod->obj); + JS::RootedObject globalProxy(aCx); + { + JSAutoRealm ar(aCx, mod->obj); + + globalProxy = CreateJSMEnvironmentProxy(aCx, mod->obj); + if (!globalProxy) { + return NS_ERROR_FAILURE; + } + } + if (!JS_WrapObject(aCx, &globalProxy)) { + return NS_ERROR_FAILURE; + } + aModuleGlobal.set(globalProxy); JS::RootedObject exports(aCx, mod->exports); if (!exports && !aIgnoreExports) { diff --git a/js/xpconnect/tests/unit/test_import_shim.js b/js/xpconnect/tests/unit/test_import_shim.js index d5b470e5eb37..b83df0a74593 100644 --- a/js/xpconnect/tests/unit/test_import_shim.js +++ b/js/xpconnect/tests/unit/test_import_shim.js @@ -200,7 +200,7 @@ function testWriteProxyOps(global, expectedNames) { add_task(function test_Cu_import_not_exported_no_shim_JSM() { // `exports` and `global` properties for not-ESM-ified case. - // Not-exported non-lexical variables should be visible in `global`. + // Not-exported variables should be visible in `global`. const exports = {}; const global = Components.utils.import( @@ -210,25 +210,31 @@ add_task(function test_Cu_import_not_exported_no_shim_JSM() { Assert.equal(global.exportedVar, "exported var"); Assert.equal(global.exportedFunction(), "exported function"); - Assert.equal(global.exportedLet, undefined); - Assert.equal(global.exportedConst, undefined); + Assert.equal(global.exportedLet, "exported let"); + Assert.equal(global.exportedConst, "exported const"); Assert.equal(global.notExportedVar, "not exported var"); Assert.equal(global.notExportedFunction(), "not exported function"); - Assert.equal(global.notExportedLet, undefined); - Assert.equal(global.notExportedConst, undefined); + Assert.equal(global.notExportedLet, "not exported let"); + Assert.equal(global.notExportedConst, "not exported const"); const expectedNames = [ + "EXPORTED_SYMBOLS", "exportedVar", "exportedFunction", + "exportedLet", + "exportedConst", "notExportedVar", "notExportedFunction", + "notExportedLet", + "notExportedConst", ]; testReadProxyOps(global, expectedNames, { - writable: true, + writable: false, enumerable: true, configurable: false, }); + testWriteProxyOps(global, expectedNames); Assert.equal(exports.exportedVar, "exported var"); Assert.equal(exports.exportedFunction(), "exported function"); @@ -252,17 +258,11 @@ add_task(function test_Cu_import_not_exported_shim() { Assert.equal(global.exportedVar, "exported var"); Assert.equal(global.exportedFunction(), "exported function"); - - // This is different than no-shim case. - // Lexical variables are visible in the shim's global. Assert.equal(global.exportedLet, "exported let"); Assert.equal(global.exportedConst, "exported const"); Assert.equal(global.notExportedVar, "not exported var"); Assert.equal(global.notExportedFunction(), "not exported function"); - - // This is different than no-shim case. - // Lexical variables are visible in the shim's global. Assert.equal(global.notExportedLet, "not exported let"); Assert.equal(global.notExportedConst, "not exported const"); @@ -276,6 +276,7 @@ add_task(function test_Cu_import_not_exported_shim() { "notExportedLet", "notExportedConst", ]; + testReadProxyOps(global, expectedNames, { writable: false, enumerable: true,