For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/proposal-ses.md.
  • 简体中文
  • SES (Secure EcmaScript) S1

    中文标题:SES(安全 ECMAScript)

    提案概览
    提案速览

    该提案引入了“compartments”(隔间)的概念,即一个 realm 内部的轻量级子 realm,旨在与共享的不可变 realm 一起使用。它规定了一个 Compartment 类和一个 lockdown() 方法,使原始对象不可变,从而防止原始对象污染并在 compartment 之间提供隔离。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    关于 SES(安全 ECMAScript)的草案提案

    请注意,此提案以前称为“proposal-frozen-realms”。然而,随着 proposal-realmsrealms-shimses-shim 的进展,我们发现不再需要将 frozen-realms 与 SES 区分开来。历史上对“Frozen Realms”的大部分引用最好被理解为指的是 SES 的旧版本。

    提案发起人

    • Mark S. Miller, Agoric
    • JF Paradis, Agoric
    • Caridy Patiño, Salesforce
    • Patrick Soquet, Moddable
    • Bradley Farias, GoDaddy, Node

    本文档规定了“compartments”(隔间),这是一个专注于创建_轻量级 realm_ 的概念,旨在与共享的_不可变 realm_ 一起使用。此处的提案旨在与各种 Realm 提案良好组合,但它是独立的。这些提案各有其效用,可以单独提出。然而,它们结合在一起比单独使用功能更强大。

    我们通过一系列示例来说明此处介绍的 SES API。

    状态

    当前阶段:

    • 第 1 阶段

    外部链接

    Moddable 的 Compartment API,此提案的直接前身,在 XS SES 引擎上实现。

    让 JavaScript 安全可靠 由 Mark S. Miller (Agoric), Peter Hoddie (Moddable) 和 Dan Finlay (MetaMask) 演讲

    向 TC53 做的演示 省略不必要的词汇

    LavaMoat - 保护您的依赖图 由 Kumavis (MetaMask) 演讲

    向 Node 安全小组做的演示 保护 ECMAScript

    历史

    安全关键型 JavaScript API 的自动化分析 由 Ankur Taly Úlfar Erlingsson John C. Mitchell Mark S. Miller Jasvir Nagra 编写

    Frozen Realms: 更安全的 JavaScript 插件的标准草案支持 是一个深入探讨重要想法的演讲,但在具体细节上已经过时。

    旧的 Realms API 提案当前的 Realms 提案。最初计划是先解决 Realms 提案,但按照目前的方法,这不再是必需的。

    在这些 Realms 之上重建 frozen realms 的原始工作是:

    摘要

    在 ECMAScript 中,一个 realm 由一个全局对象和一组关联的_原始对象_组成 -- 这些是像 Array.prototype 这样的可变对象,必须在任何代码运行之前存在。一个 realm 内的对象隐式共享这些原始对象,因此很容易通过_原始对象污染_ -- 修改这些对象使其行为异常 -- 来相互干扰。这种干扰可能是偶然的,也可能是恶意的。如今,在浏览器中,可以通过_同源 iframe_ 创建 realm,在 Node 中则可以通过 vm 上下文创建。在创建时,这些 realm 彼此分离,因为它们不共享可变状态。由于原型是可变的,每个 realm 需要自己的一组,这使得这种分离在细粒度上使用成本过高。

    Realm 目前不直接暴露给 JavaScript,但在规范中用 realm 记录 表示,其中最重要的槽位是_内部对象 (intrinsics)全局对象* 和_全局词法环境*(参见 ECMA262 第 8.2 节 Realms)。

    我们提议添加 compartments(隔间)的概念,以指定一个 realm 内部的_轻量级子 realm_。每个 compartment 都有自己的全局对象和全局词法作用域,但给定 realm 内的所有 compartment 共享它们的内部对象。通过使内部对象不可变来实现隔离,防止一个 compartment 中的对象污染其他 compartment 使用的原型。

    这意味着每个 compartment 由一个新的_全局对象_和一个新的_全局词法环境_组成:

    记录槽位RealmCompartment
    内部对象 (intrinsics)可变不可变,共享
    全局对象可变可变
    全局词法作用域可变可变

    compartment 记录_类似于_realm 记录,只是它的_internal (intrinsics)_ 槽位指向父 realm 记录。在规范中所有引用 realm 记录的地方,compartment 记录都可以直接替换,无需进一步更改。

    Compartment 构造函数

    已被 Compartments 提案取代。

    我们提议一个 Compartment 类,其实例是上述“compartment”概念的体现,用于在给定 realm 内创建多个_轻量级子 realm_。

    尽管最初是独立的,但 compartments 可以通过全局对象和模块彼此进行密切接触。

    class Compartment {
      constructor: (
        endowments: object?,     // 添加到全局对象的额外绑定
        moduleMap: object?,      // 将子说明符映射到父说明符
        options: object?         // 包括诸如 isDirectEvalHook 之类的钩子
      ) -> object                // 一个奇异的 compartment 对象
    
      get global -> object       // 访问此 compartment 的全局对象
    
      evaluate(                  // 在此 compartment 中执行严格的间接 eval
        src: stringable,
        options: object?         // 每次评估而非每个 compartment 的选项
      ) -> any
    
      // 与动态导入相同的签名
      async import(specifier: string) -> promise<ModuleNamespace>
      importSync(specifier: string) -> ModuleNamespace
    }

    compartment 构造函数创建一个新的轻量级子 realm,带有新的 global、新的 eval 函数、新的 Function 构造函数和新的 Compartment 构造函数。

    • compartment 全局对象包含 ECMA262 定义的所有原始状态,但不包含宿主提供的对象,因此 windowdocumentXMLHttpRequestrequireprocess 等都不存在。因此,compartment 不包含与外部世界(如用户或网络)交互所需的任何对象。

    • 新的 evalFunctionCompartment 将在新 compartment 的全局作用域中评估代码:新 compartment 的 global 成为它们的全局对象。

    • 新的 evalFunctionCompartment 继承自共享的 %FunctionPrototype%。

    • 新的 Function.prototype 是共享的 %FunctionPrototype%。

    • 新的 Compartment 构造函数...?

    • 新的 Compartment.prototype 是共享的 %CompartmentPrototype%。

    然后,构造函数将 endowments 参数的自有可枚举属性的值复制到新的 global 上,并返回新的 compartment 实例。通过这些额外的赋与 (endowments),用户提供了他们希望在生成的 compartment 中可用的_虚拟宿主对象_。

    Compartment 构造函数只有在调用 lockdown(见下文)后才在全局对象上可用。

    Compartment 原型

    我们提议在共享的 Compartment.prototype 上,供所有 Compartment 类的实例继承:

    • 一个 global 获取器,用于提供对 compartment 全局对象的访问。其行为类似于 globalThis 全局对象。
    • 一个 evaluate 方法,用于在新 compartment 的全局作用域中评估代码。其签名与 eval() 函数相同,但可能带有一个额外的可选 options 参数。
    • 一个异步的 import 方法,用于动态加载新 compartment 中的模块。其签名与动态导入函数相同。

    lockdown 方法

    我们提议一个静态方法,lockdown()Realm.lockdown(),用于将当前 realm 转换为具有不可变原始对象的状态。我们称这样的 realm 为_不可变 realm_。Realm 全局对象将由 Realms 提案规定。

    lockdown 操作包括:

    • 驯服一些全局对象(见下文)。
    • 驯服函数构造函数(见下文)。
    • 冻结所有内部对象(见下文)。
    • 禁用导致_覆盖错误 (override mistake)_ 的默认机制(见下文)。
    • 通过全局对象暴露 Compartment 构造函数,该构造函数在 lockdown 之前不可用(见下文)。

    尽管 CompartmentRealm.lockdown() 看起来是正交的,但它们只有在直接组合使用时才变得有趣:

    Realm.lockdown();
    const cmpA = new Compartment();
    const cmpB = new Compartment();

    lockdown 之后,cmpAcmpB 共享的所有原始对象都是不可变的,因此两者都不能毒害对方的原型。由于它们不共享可变状态,它们之间的完全分离程度相当于由两个同源 iframe 创建的两个完整 realm(除了冻结原始对象的共享身份,从而避免了下面解释的身份不连续性)。

    在调用 lockdown 之前允许修改原型。(这就引发了一些关于 lockdown 冻结了什么的有趣问题)。

    (用接下来的两段编辑)

    一个长期公认的最佳实践是“不要猴子补丁原始对象”-- 不要修改任何原始状态。遵循此实践的大多数遗留代码已经兼容于从不可变根 realm 派生的轻量级 realm。本文档的其余部分将解释一些进一步的限定条件。

    如果需要对内部对象进行自定义,可以在调用 lockdown 之前以及创建任何 compartment 之前进行。

    Compartment 全局对象

    compartment 构造函数在调用 lockdown() 之前不可用,以避免省略 lockdown 并创建带有未冻结原始对象的 compartment 的风险(这无法提供预期的隔离)。

    冻结内部对象和驯服全局对象

    为了安全共享内部对象,它们必须是传递不可变的。幸运的是,在 ES2016 的标准原始对象中,唯一可变的原始状态是:

    • 原始对象的可变自有属性
    • 原始对象的可变内部 [[Prototype]] 槽位
    • 添加属性的能力
    • Math.random
    • Date.now
    • 无参数调用的 Date 构造函数
    • 作为函数调用的 Date 构造函数,无论参数如何 (这令我很惊讶!)
    • 规范性可选提议的 RegExp 静态方法(链接)
    • 规范性可选提议的 Error.prototype.stack 访问器(链接)

    为了制作一个传递不可变的根 realm,我们分别需要:

    • 移除所有非标准属性
    • 移除 Math.random
    • 移除 Date.now
    • new Date() 抛出 TypeError
    • Date(any...) 抛出 TypeError
    • 如果存在,移除 RegExp 静态方法
    • 移除 RegExp.prototype.compile
    • 如果存在,移除 Error.prototype.stack
    • 使所有原始对象不可扩展。
    • 使所有剩余属性不可配置,不可写。如果是一个访问器属性,我们规定其 getter 必须总是返回相同的值,而不修改任何状态,并且其 setter 要么不存在要么在不修改任何状态的情况下抛出错误。

    同样,任何规范的新增内容也需要遵循相同的策略,以避免在 compartment 中引入可变状态。

    用户可以在需要时有效地将 DateMath 的缺失功能添加回来,或者替换为安全的实现。例如:

    const DateNow = Date.now;
    
    Realm.lockdown();
    
    function unsafeDate() {
      return Date(...arguments);
    }
    Object.defineProperties(unsafeDate, Object.getOwnPropertyDescriptors(Date));
    Object.defineProperty(unsafeDate, 'now', {
    	value: DateNow,
    	writable: true,
    	enumerable: false,
    	configurable: true
    });
    
    const cmp = new Compartment({ Date: unsafeDate });

    驯服函数构造函数。

    所有内部对象都是共享的,但 %Function%、%GeneratorFunction%、%AsyncFunction% 和 %AsyncGeneratorFunction% 默认在 realm 的全局作用域中执行源代码评估。

    lockdown 之后,这些构造函数应该被替换为抛错而不是评估源代码的函数,以便可以安全共享。 我们可以规定它们的抛错行为与宿主钩子(适用于 CSP)抑制评估时的行为相同,将其映射到已经可能的行为。 如果 Compartment 是每个 realm 的全局对象而不是每个 Compartment 的全局对象,那么 Compartment.prototype.constructor === Compartment,这是否需要被驯服?让我们讨论一下。

    覆盖错误 (Override mistake)

    由于当时缺乏足够的远见,ES5 不幸地规定,如果对不存在属性的简单赋值会覆盖同名非可写数据属性,则该赋值必须失败。(事后看来,这是一个错误,但现在为时已晚,我们必须承担后果。)这与类和对象字面量的覆盖方式不一致,因为它们执行 [[DefineOwnProperty]] 而不是赋值。

    因此,仅仅冻结对象以使其不可变,会产生一个不幸的副作用,即破坏先前被认为是遵循 JS 最佳实践的正确代码,如果这些先前代码使用赋值进行覆盖的话。例如,这个赋值将会失败:

    Object.freeze(Array.prototype);
    const arr = []
    arr.join = true; // 在严格模式下抛出,在非严格模式下忽略。

    因此,在冻结原始对象之后,我们需要 使不可写的原型属性不阻止对实例的赋值

    参见 覆盖错误。(有更好的链接吗?)

    (我们需要另一个语义状态位来区分这两种冻结方式。我们应该规定 petrify 甚至 harden 也能防止覆盖错误,尽管我们避免完全 shim 它。)

    身份不连续性

    两个由同源 iframe 或 vm 上下文创建的 realm 可以相互接触。一旦接触,它们可以自由地混合它们的对象图。当 realm 这样做时,它们会遇到一种不便和错误来源,我们在这里称之为_身份不连续性_。例如,如果来自 iframeA 的代码创建了一个数组 arr 并传递给来自 iframeB 的代码,并且 iframeB 测试 arr instanceof Array,答案将是 false,因为 arr 继承自 iframeA 的 Array.prototype,它不同于 iframeB 的 Array.prototype

    相比之下,由于 cmpAcmpB 共享相同的 Array.prototype,由一个创建的数组 arr 仍然可以通过另一个测试的 arr instanceof Array

    ###################################

    下面待办

    限制示例

    function confine(src, endowments) {
      return sharedRoot.spawn(endowments).eval(src);
    }

    这个 confine 函数是一个安全抽象的示例,我们可以通过组合上述原语轻松构建。它使用 spawn 从我们上面不可变的 sharedRoot realm 创建一个轻量级子 realm,将 endowments 的自有可枚举属性复制到该轻量级 realm 的全局对象上,然后在该全局对象的作用域中评估 src 并返回结果。这个 confine 函数对于_对象能力_编程特别有用。这些原语(连同 membranes)也可以帮助支持其他安全模型,例如_去中心化动态信息流_,尽管可能还需要更多的机制。我们尚未对此进行详细探讨。

    confine 函数来自 SES,它有一个形式语义 支持对 SES 代码的某些安全属性进行自动验证。它是作为 Google Caja 项目的一部分开发的;你可以在 Caja 网站上阅读更多关于 SES 和 Caja 的信息。)

    confine('x + y', {x: 3, y: 4})  // -> 7
    
    confine('Object', {})           // -> 不可变根的 Object 构造函数
    
    confine('window', {})           // ReferenceError, 作用域中没有 'window'

    插件分离示例

    function Counter() {
      let count = 0;
      return Object.freeze({
        incr: Object.freeze(() => ++count),
        decr: Object.freeze(() => --count)
      });
    }
    const counter = new Counter();
    
    // ...从未受信任的客户端获取 billSrc 和 joanSrc...
    const bill = confine(billSrc, {change: counter.incr});
    const joan = confine(joanSrc, {change: counter.decr});

    假设上面的代码由一个我们称为 Alice 的程序执行。在此代码中,Alice 获取插件 Bill 和 Joan 的源代码。Alice 不知道这些插件编写得有多好,因此希望保护自己免受它们的不良行为影响,同时保护它们各自免受对方的不良行为影响。Alice 是否担心意外或恶意的行为无关紧要。

    使用上面的代码,Alice 向她定义的插件框架所特有的插件展示了她设计的 API 表面。在这个简单的例子中,她向每个插件提供了一个他们称之为 change 的函数,用于操作共享计数器的状态。通过调用他的 change 变量,Bill 只能递增计数器并查看结果。通过调用她的 change 变量,Joan 只能递减计数器并查看结果。通过使用她的 counter 变量,Alice 可以同时执行两者。

    如果上面 Alice 的代码是普通的 JavaScript 代码,那么她没有实现这个目标。例如,Bill 或 Joan 可以使用表达式 change.__proto__ 来访问并污染 Alice 的原型,并以 Alice 未预期的方式相互交互。Alice 暴露给 Bill 和 Joan 的 API 表面不是_防御性的_;它不能保护自己和 Alice 免受 Bill 和 Joan 的不良行为影响。

    如果上面 Alice 的代码是在从不可变根 realm 派生的 realm 中评估的,那么它是正确防御的。Alice 将 Bill 和 Joan 置于这样的 realm 中以限制他们。她将自己置于这样的 realm 中以获得_可防御性_,Alice 可以使用它来定义可以安全暴露给 Bill 和 Joan 的防御性抽象。如果 Alice、Bill 和 Joan 都源自 sharedRoot,那么他们的进一步交互是可防御的,并且没有身份不连续性。

    便利函数:def(obj)

    上面所有对 Object.freeze 的调用都很丑陋。Caja def(obj) 函数是应该由库提供的便利函数的示例。它通过遵循属性和 [[Prototype]] 链接,从 obj 开始递归地将 Object.freeze 应用于它找到的所有对象。这为所有这些对象提供了一个防篡改的 API 表面(请注意,除非在特殊情况下,它 不会 使它们不可变)。名称 def 代表“_定义一个_可防御的_对象”。

    使用 def,我们可以将我们的 Counter 示例代码重写为:

    function Counter() {
      let count = 0;
      return def({
        incr() { return ++count; }
        decr() { return --count; }
      });
    }

    为了高效,def 需要以某种方式与本提案结合,以便在遇到任何这些传递不可变的原始对象时知道停止遍历。我们将这个集成问题留给以后的提案来解决。

    Compartments 示例

    通过组合 可撤销 membraneconfine,我们可以制作 compartments:

    function makeCompartment(src, endowments) {
      const {wrapper,
             revoke} = makeMembrane(confine);
      return {wrapper: wrapper(src, endowments),
              revoke};
    }
    
    // ...从未受信任的客户端获取 billSrc 和 joanSrc...
    const {wrapper: bill,
           revoke: killBill} = makeCompartment(billSrc, endowments);
    const {wrapper: joan,
           revoke: killJoan} = makeCompartment(joanSrc, endowments);
    
    // ... 向彼此介绍相互怀疑的 Bill 和 Joan...
    // ... 同时使用两者 ...
    killBill();
    // ... Bill 对我们和 Joan 来说都不可访问。GC 可以收集 Bill ...

    killBill 被调用后,Bill 代码无法再产生任何进一步的影响。

    详细提案

    你可以在 ecmarkup 格式查看规范文本草案,或者呈现为 HTML

    1. 引入 Realm 类作为 ECMAScript 标准 API 的官方认可部分。

    2. Realm 类中添加一个静态方法 Realm.immutableRoot(),它获取一个_不可变根 realm_,其中所有原始对象已经是传递不可变的。这些原始对象包括 ES2016 中定义为强制性的所有原始对象。(以及截至撰写本文时(2016年3月17日)草稿 ES2017 中的那些。)这些原始对象必须不包含此处指定之外的任何其他对象或属性。在不可变根 realm 中,全局对象本身也是传递不可变的。具体来说,它不包含任何主机特定的对象。这个冻结的全局对象是一个普通对象,其 [[Prototype]]Object.prototype,即该不可变根 realm 的 %ObjectPrototype% 内部对象。

      • 由于两个不可变根 realm 除了对象身份之外在所有方面都永远相同,我们将 Realm.immutableRoot() 是总是创建一个新的,还是总是返回同一个,保留为实现定义。在任何给定的实现上,它必须要么总是新的,要么总是相同的。
    3. 为了实现不可变根 realm 所需的深度不可变性,其两个原始对象必须从现有标准进行修改:不可变根 realm 的 Date 对象移除了其 now() 方法,并且其默认构造函数改为抛出 TypeError,而不是揭示当前时间。不可变根 realm 的 Math 对象移除了其 random() 方法。

    4. Realm 类中添加一个实例方法 spawn(endowments)

      1. spawn 创建一个新的轻量级子 realm,带有自己的新全局对象(在下面用符号 freshGlobal 表示),其 [[Prototype]] 是父 realm 的全局对象。这个新的全局对象也是一个普通对象。与不可变根 realm 的全局对象不同,这个新的 freshGlobal 默认_不_被冻结。

      2. spawn 用具有全局名称的评估器(目前只有 evalFunction)的覆盖绑定填充这个 freshGlobal。它将这些名称中的每一个绑定到新的对象,这些对象的 [[Prototype]] 是来自父 realm 的相应对象。

      3. spawnendowments 记录的自有可枚举属性复制到 freshGlobal 上。

      4. spawn 返回那个新的子 realm 实例。

      轻量级 realm 的总成本是四个对象:realm 实例本身、freshGlobal,以及特定于它的 eval 函数和 Function 构造函数。

    5. 生成的 realm 的评估器在该 realm 全局对象的作用域中评估代码,使用该全局对象作为它们的全局对象。

      轻量级 realm 的初始 eval 继承自其父级的 eval。对于每个覆盖的构造函数(目前只有 Function),其 prototype 属性初始时具有与其继承的构造函数相同的值。因此,来自一个后代 realm 的函数 foo 使用另一个来自同一父 realm 的后代的 Function 构造函数通过 foo instanceof Function 测试。在兄弟轻量级 realm 之间,对原始类型的 instanceof 可以正常工作。

    Polyfill 示例

    在下面的 要点 部分,我们解释了激发移除 Date.nowMath.random 的非公开信道威胁。然而,通常这种威胁并不令人关注,这种情况下我们宁愿包含 ES2016 的完整 API,因为除此之外它是安全的。事实上,Caja 一直提供 DateMath 的完整功能,因为 Caja 的威胁模型并不要求拒绝它们。

    以下 makeColdRealm(GoodDate, goodRandom) 函数,给定一个良好的 Date 构造函数和 Math.random 函数,制作一个新的足够冻结的轻量级 realm,可以像使用不可变根 realm 一样使用它 -- 作为生成轻量量子 realm 的生成根。这些子 realm 彼此之间足够分离,如果不担心非公开信道的话。与直接从不可变根 realm 派生的轻量级 realm 不同,从共同的冷 realm 生成的子 realm 共享功能完整的 DateMath

    function makeColdRealm(GoodDate, goodRandom) {
      const goodNow = GoodDate.now;
      const {Date: SharedDate, Math: SharedMath} = sharedRoot;
      function FreshDate(...args) {
        if (new.target) {
          if (args.length === 0) {
            args = [+goodNow()];
          }
          return Reflect.construct(SharedDate, args, new.target);
        } else {
          return String(GoodDate());
        }
      }
      FreshDate.now = () => +goodNow();
      FreshDate.prototype = SharedDate.prototype;  // 以便 instanceof 正常工作
      FreshDate.name = SharedDate.name;
      FreshDate.__proto__ = SharedDate;
    
      const FreshMath = {
        __proto__: SharedMath,
        random() { return +goodRandom(); }
      };
    
      return def(sharedRoot.spawn({Date: FreshDate, Math: FreshMath}));
    }

    除了 DateMath,我们可以创建抽象来赋予新的全局对象对预期宿主提供的全局对象(如 windowdocumentXMLHttpRequest)的虚拟化模拟。这些模拟可以映射到调用者自己的对象,也可以不映射。Caja 的 Domado 库 正是使用这种技术来模拟大多数常规浏览器和 DOM API,方法是将受限代码的虚拟 DOM 映射到调用者指定的调用者“物理” DOM 部分。从这个意义上说,受限代码就像操作系统中的用户模式代码,其虚拟内存访问通过它看不到或无法控制的映射映射到物理内存。Domado 以类似的方式重新映射 URI 空间。通过模拟浏览器 API,许多现有的浏览器代码可以在由调用者使用 Domado 配置的虚拟化浏览器环境中兼容运行。

    由于 evalFunction 以及上面的 DateMath 在可观察性上遮蔽了它们父 realm 中的对应对象,生成的环境不是标准 ECMAScript 的完全忠实模拟。然而,这些打破幻想的做法是避免从共同父级生成的轻量级 realm 之间身份不连续性的必要代价。我们仔细选择了这些打破点,以便与几乎所有不是专门编写来测试标准符合性的代码兼容。

    移动代码示例

    Map-Reduce 框架生动地展示了将代码发送到数据而不是将数据发送到代码的强大功能。灵活的分布式计算系统必须能够表达两者。

    既然 Function.prototype.toString 将提供可靠可评估的字符串 可以被发送,那么不可变根 realm 为接收者提供了一种安全的方式来评估它,以便以安全的方式重建该函数的调用行为。假设我们有一个 RemotePromise 构造函数,它创建一个针对位于其他地方对象的远程承诺,可能位于另一台机器上。下面,假设 RemotePromise 构造函数将此远程承诺的私有实例变量 #farEval 初始化为另一个远程承诺,针对该承诺的履行位置(vat、worker、agent、事件循环、place 等)处不可变根 realm 的 eval 方法。如果此承诺拒绝,则其 #farEval 承诺同样会拒绝。

    class QPromise extends Promise {
      // ... 来自 https://github.com/kriskowal/q/wiki/API-Reference 的 API
      // 我们在下面实际用到的全部是 fcall
    }
    
    // 参见 https://github.com/kriskowal/q-connection
    class RemotePromise extends QPromise {
      ...
      // callback 必须是一个闭包函数,即唯一的自由变量是 ES2016 定义的全局变量,因此存在于 proto-global 上。
      there(callback, errback = void 0) {
        const callbackSrc = Function.prototype.toString.call(callback);
        const farCallback = #farEval.fcall(callbackSrc);
        return farCallback.fcall(this).catch(errback);
      }
    }

    我们通过类比来解释 there。熟悉的表达式 Promise.resolve(p).then(callback)callback 函数推迟到承诺 p 被履行后的未来某个时间。类似地,表达式 RemotePromise.resolve(r).there(callback) 将闭包函数 callback 推迟并迁移到未来某个时间和空间,即履行后的远程承诺 r 将指定的对象所在位置。thenthere 都返回一个承诺,表示 callbackerrback 将返回的内容。

    这支持一种异步分区全局地址空间 的联邦形式,这是 X10 超级计算机语言使用的并发模型,与我们的处理异步的承诺框架平滑集成。

    确定性如何?

    我们不将任何形式的回放包含在本提案的目标中,因此这个“确定性如何”部分仅仅因为本节末尾的要点而重要。

    给定一个确定性的规范,人们可以确定两个计算,在两个符合规范的实现上运行,从相同的状态开始并输入相同的输入,将计算出相同的新状态和输出。ES5 和 ES2015 规范非常接近确定性。ECMAScript 避免了一些常见但非必要的不确定性来源,如 Java 的 System.identityHashCode 或身份哈希表的枚举顺序。但 ECMAScript 规范由于三个原因而未能实现确定性:

    • 真正的不确定性,例如 Math.random()
    • 未指定但不可避免的失败,例如内存不足。
    • 明确的欠规范,即留下一些可观察的行为由实现决定。

    明确不确定地感知当前时间(通过 new Date()Date.now())或生成随机数(通过 Math.random())的能力在不可变根 realm 中被禁用,因此默认情况下在从它生成的每个 realm 中也被禁用。新的不确定性来源,如 makeWeakRefgetStack,不会被添加到不可变根 realm 中,或者会被类似地禁用。

    迄今为止的 ECMAScript 规范从未承认存在内存不足等失败的可能性。理论上,这意味着符合规范的 ECMAScript 实现需要一台无限内存的机器。不幸的是,目前很难获得这样的机器。由于 ECMAScript 是一种隐式分配的语言,内存不足的情况可能导致计算在任何时候失败。如果这些失败通过不可预测地抛出可捕获异常 报告,那么防御性编程将变得不可能。这将与许多 ECMAScript 代码 的目标背道而驰。因此,任何希望捍卫其不变量的 ECMAScript 计算,以及与它纠缠的任何同步计算,在遇到不可预测的错误时,必须先发制人地中止,而不再运行任何用户代码

    即使 ECMAScript 在其他方面是可确定性重放的,这些不可预测的先发制人失败也会阻止它。我们转而检查故障停止确定性这个较弱的属性,其中每个副本要么失败,要么以与其他所有未失败副本相同的方式成功。

    虽然数量很少,但确实有一些规范问题明显地留给了实现,实现之间可能会有所不同。其中一些最终可能会通过未来的 TC39 协议来解决,例如如果在枚举期间修改对象时的枚举顺序(待办链接)。其他的,如 Array.prototype.sort 使用的排序算法,不太可能被解决。然而,实现定义 并不一定是真正的不确定性。在给定的实现上,仅由实现定义的操作在该实现的范围内可以是确定性的。它们应该在同一个实现上运行时是可故障停止重现的。然而,为了利用这一点进行重放,我们需要确定“同一实现”的含义,这似乎难以捉摸且困难。

    要点

    即使不确定“实现定义”的确切含义,一个仅限于故障停止实现定义确定性的计算_无法读取它未被明确启用读取的隐蔽信道和侧信道_。实际上没有什么可以阻止在隐蔽信道和侧信道上的信号传输,但近似的确定性可以实际阻止受限计算感知这些信号。

    (待办:解释人择侧信道及其与信息流终止信道的区别。)

    故障停止实现定义确定性对测试和调试巨大的福音。所有非确定性的依赖项,如所谓的当前时间,都可以以可重现的方式被模拟和注入

    Annex B 注意事项

    截至 ES2016,Annex B 的规范性可选部分对于作为不可变根 realm 的规范性可选部分包含是安全的。然而,Annex B 规定这些在网络浏览器中是规范性强制的,而对于不可变根 realm 则没有这样的要求。即使在网络浏览器中运行,一个不可变根 realm,由于没有主机特定的全局对象,必须被视为非浏览器环境。一些 ES2015 之后为 Annex B 提议的 API,例如 RegExp 静态属性Error.prototype.stack 访问器属性,对于包含在不可变根 realm 中是不安全的,因此必须不存在。

    目前,为了最大限度地兼容普通 ECMAScript,我们不改变不可变根 realm 的评估器,使其默认以严格模式评估代码。然而,我们应该考虑这样做。人们希望在不可变根 realm 下运行的大部分代码,包括遗留代码,可能已经与严格模式兼容。从不可变根 realm 及其生成的后代中省略非严格模式也会使章节 B.1.1B.1.2B.3.2B.3.3B.3.4 变得无关紧要。目前尚不清楚不可变根 realm 的评估器应如何规定 B.1 节中剩余的规范性可选语法。但这些评估器接受的语法,至少在严格模式下,很可能应该由规范精确确定。

    Annex B 的某些元素是安全的,并且在实践中可能是强制性的,与主机环境无关:

    • escapeunescape
    • Object.prototype.__proto__
    • String.prototype.substr
    • 根据内部 CreateHTML 定义的 String.prototype 方法:anchorbig、...、sup
    • Date.prototype.getYearDate.prototype.setYear
    • Date.prototype.toGMTString
    • 对象初始化器中的 __proto__ 属性名

    除最后一项外,所有这些都已在 Caja 的 SES-shim 中被列入白名单 很长时间而没有问题。(最后一项是语法,因此不受 SES-shim 白名单机制的限制。)

    讨论

    由于不可变根 realm 是传递不可变的,我们可以安全地在完全隔离的 ECMAScript 程序之间共享它。这种共享使他们能够访问共享对象和共享身份,但没有能力相互通信或影响自身之外的任何状态。我们甚至可以在源和目标之间以及线程之间共享不可变根 realm,因为规范级别的深度不可变性应该使实现级别的线程安全变得简单。

    如今,要用 ECMAScript 编写自托管内置函数,必须实践安全元编程 技术,以便这些内置函数具有适当的防御性。这种技术很难正确掌握,特别是如果这种自托管向 ECMAScript 嵌入者开放。相反,这些内置函数可以在从不可变根 realm 派生的轻量级 realm 中定义,从而更容易以更高的置信度实现防御性。

    根据上述规则,生成的 realm 的 Function.prototype.constructor 将是父 realm 的 Function 构造函数,即与生成的 realm 的 Function.__proto__ 相同。作为这种奇特拓扑的交换,我们获得了 instanceof 在生成的 realm 之间默认透明工作的良好属性 -- 除非被用户的 polyfill 覆盖为相反。

    在 ES2016 中,GeneratorFunction 评估器不是命名的全局对象,而是未命名的内部对象。即将推出的评估器可能包括 AsyncFunctionAsyncGeneratorFunction。这些也可能被规定为未命名的内部对象。对于所有这些问题,上述基于名称的 spawn 覆盖都是无关紧要的,可能也不需要。

    由于在不可变根 realm 内评估的代码无法在其自身之外引起任何影响,除非它被明确授予访问权限,因此不可变根 realm 的评估器应该即使在 CSP 禁止正常评估器 的环境中也能继续运行。类似地,CSP 评估器抑制不会抑制 JSON.parse。在不可变根 realm 中评估代码几乎没有比 JSON 数据更危险的方式。

    其他可能的提案,如私有状态可防御的 const,可能有助于防御性编程,这种编程在本提案的背景下尤其强大。但由于这种防御性编程支持的效用不仅限于 frozen realms,它们应该保持为独立的提案。

    对于每个即将提出的本质上不是不可变且无能力的标准 API:

    它们必须从不可变根 realm 中缺席,或者它们的行为被大幅截断为安全的行为。本规范还需要说明它们最初如何(如果有的话)出现在每个单独生成的轻量级 realm 中。特别是,我们期望出现一种模式,即为新生成的 realm 创建新的 loader 实例作为其默认 loader。一旦一些提议的 API 被规定为通过从内置原始模块 导入来提供,我们将需要解释它们如何出现在不可变根 realm 和/或其生成的 realm 中。

    开放问题

    • Realm.immutableRoot() 应该每次返回一个新的新鲜的冻结 realm,还是应该总是返回同一个?上面我们暂时将其保留为实现定义,以鼓励实现进行实验,看看每个实现可以达到多高的效率。如果所有人都能就其中一个选项达成一致,我们应该将其编纂成文,而不是继续将其保留为实现定义。

    • 尽管在 TC39 的管辖范围内这不是一个正式问题,但我们应该讨论现有的 CSP“禁止脚本评估”设置是否应该豁免不可变根 realm 的评估器,或者是否应该扩展 CSP 以表达这种差异禁止。

    • 目前,如果 eval 的值不是 eval 的原始值,那么以直接-eval 表达式的形式使用它的任何用法实际上将具有间接 eval 的语义,即,对 eval 当前值的简单函数调用。如果不可变根 realm 的内置评估器默认不严格,那么任何用户自定义(用默认严格的包装器替换生成的 realm 的全局评估器)都会破坏它们用于直接-eval 的功能。幸运的是,这似乎可以通过旧 Realms API 的其余部分来解决。

    • 标准的 Date 构造函数在以下情况下会揭示当前时间

      • 无参数作为构造函数调用时,或
      • 作为函数调用时(无论参数如何)

      上面我们提议通过让 proto-Date 构造函数在这些情况下抛出 TypeError 来审查当前时间。其他错误类型是否更合适?与其抛出错误,new Date() 是否应该产生一个无效日期,等同于 new Date(NaN) 产生的日期?如果是这样,将 Date 构造函数作为函数调用应该产生相应的字符串 "Invalid Date"。如果我们朝这个方向走,甚至可以设想让 Date.now() 返回 NaN。相反,移除 Date.now 的优点在于支持 ECMAScript 程序员实践的 feature-testing 风格。

    • 当然,还有关于名称的没完没了的争论。我们并不执着于我们在这里呈现的名称。

    规范文本

    更新本提案的规范文本

    规范文本的来源位于 spec/index.emu,并使用 ecmarkup 语言编写。

    修改规范文本时,你应该能够通过使用以下命令将 HTML 版本构建到 index.html 中:

    npm install
    npm run build
    open index.html

    或者,你可以使用 npm run watch

    致谢

    这里提出的 Compartment API 直接源于 Moddable 早期的 Compartment API,在独立 SES 的 XS 实现中。我们特别感谢 Patrick Soquet 和 Peter Hoddlie 进行多次头脑风暴和优化会议。

    感谢最近 SES 会议的常客,特别是 Bradley Farias、Michael Fig、Saleh Motaal 和 Chip Morningstar。

    非常感谢 E. Dean Tribble、Kevin Reid、Dave Herman、Michael Ficarra、Tom Van Cutsem、Kris Kowal、Kevin Smith、Terry Hayes、Daniel Ehrenberg、Ojan Vafai、Elliott Sprehn 和 Alex Russell。感谢整个 Caja 团队(Jasvir Nagra、Ihab Awad、Mike Stay、Mike Samuel、Felix Lee、Kevin Reid 和 Ben Laurie)构建了一个所有最困难的问题都已解决的系统。