-
Notifications
You must be signed in to change notification settings - Fork 72
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(AsyncContext): examine security of AsyncContext
- Loading branch information
Showing
18 changed files
with
1,017 additions
and
0 deletions.
There are no files selected for viewing
33 changes: 33 additions & 0 deletions
33
packages/eventual-send/src/async-contexts/0-sync-context-original.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// Based on slide 6 of | ||
// https://docs.google.com/presentation/d/1yw4d0ca6v2Z2Vmrnac9E9XJFlC872LDQ4GFR17QdRzk/edit#slide=id.g18e6eaa50e1_0_192 | ||
|
||
// The original __storage__ was initialized to `new Map()`, but nothing | ||
// was ever stored into it. To make clearer that this *initial* binding | ||
// of `__storage__` does not carry state, we initializa it to `undefined` | ||
// instead, and adjust `get()` to compensate. | ||
|
||
// eslint-disable-next-line no-underscore-dangle | ||
let __storage__; | ||
|
||
export class SyncContext { | ||
constructor() { | ||
harden(this); | ||
} | ||
|
||
run(val, cb, args = []) { | ||
const prev = __storage__; | ||
const next = new Map(__storage__); | ||
next.set(this, val); | ||
try { | ||
__storage__ = next; | ||
return cb(...args); | ||
} finally { | ||
__storage__ = prev; | ||
} | ||
} | ||
|
||
get() { | ||
return __storage__ && __storage__.get(this); | ||
} | ||
} | ||
harden(SyncContext); |
20 changes: 20 additions & 0 deletions
20
packages/eventual-send/src/async-contexts/1-sync-context-maker.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// eslint-disable-next-line no-underscore-dangle | ||
let __storage__; | ||
|
||
export const makeSyncContext = () => | ||
harden({ | ||
run: (val, cb, args = []) => { | ||
const prev = __storage__; | ||
const next = new Map(__storage__); | ||
next.set(this, val); | ||
try { | ||
__storage__ = next; | ||
return cb(...args); | ||
} finally { | ||
__storage__ = prev; | ||
} | ||
}, | ||
|
||
get: () => __storage__ && __storage__.get(this), | ||
}); | ||
harden(makeSyncContext); |
18 changes: 18 additions & 0 deletions
18
packages/eventual-send/src/async-contexts/2-sync-context-shallow.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
export const makeSyncContext = () => { | ||
let state; | ||
|
||
return harden({ | ||
run: (val, cb, args = []) => { | ||
const prev = state; | ||
try { | ||
state = val; | ||
return cb(...args); | ||
} finally { | ||
state = prev; | ||
} | ||
}, | ||
|
||
get: () => state, | ||
}); | ||
}; | ||
harden(makeSyncContext); |
21 changes: 21 additions & 0 deletions
21
packages/eventual-send/src/async-contexts/3-sync-context-deep.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// eslint-disable-next-line no-underscore-dangle | ||
let __get__ = harden(_k => undefined); | ||
|
||
export const makeSyncContext = () => { | ||
const key = harden({}); | ||
|
||
return harden({ | ||
run: (val, cb, args = []) => { | ||
const prev = __get__; | ||
try { | ||
__get__ = harden(k => (k === key ? val : prev(k))); | ||
return cb(...args); | ||
} finally { | ||
__get__ = prev; | ||
} | ||
}, | ||
|
||
get: () => __get__(key), | ||
}); | ||
}; | ||
harden(makeSyncContext); |
23 changes: 23 additions & 0 deletions
23
packages/eventual-send/src/async-contexts/4-sync-context-transpose.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// eslint-disable-next-line no-underscore-dangle | ||
let __get__ = harden(_m => undefined); | ||
|
||
export const makeSyncContext = () => { | ||
const transposedMap = new WeakMap(); | ||
|
||
return harden({ | ||
run: (val, cb, args = []) => { | ||
const prev = __get__; | ||
const key = harden({}); | ||
transposedMap.set(key, val); | ||
try { | ||
__get__ = harden(m => (m.has(key) ? m.get(key) : prev(m))); | ||
return cb(...args); | ||
} finally { | ||
__get__ = prev; | ||
} | ||
}, | ||
|
||
get: () => __get__(transposedMap), | ||
}); | ||
}; | ||
harden(makeSyncContext); |
51 changes: 51 additions & 0 deletions
51
packages/eventual-send/src/async-contexts/5-async-context-original.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
// Based on 0-sync-context-original.js and slides 11 and 13 of | ||
// https://docs.google.com/presentation/d/1yw4d0ca6v2Z2Vmrnac9E9XJFlC872LDQ4GFR17QdRzk/edit#slide=id.g18e6eaa50e1_0_192 | ||
|
||
// See note in 0-sync-context-original.js about initially binding `__storage__` | ||
// to `undefined` rather than `new Map()`. | ||
|
||
// eslint-disable-next-line no-underscore-dangle | ||
let __storage__; | ||
|
||
export class AsyncContext { | ||
constructor() { | ||
harden(this); | ||
} | ||
|
||
run(val, cb, args = []) { | ||
const prev = __storage__; | ||
const next = new Map(__storage__); | ||
next.set(this, val); | ||
try { | ||
__storage__ = next; | ||
return cb(...args); | ||
} finally { | ||
__storage__ = prev; | ||
} | ||
} | ||
|
||
get() { | ||
return __storage__ && __storage__.get(this); | ||
} | ||
} | ||
harden(AsyncContext); | ||
|
||
// Exposed only to the internal `then` function? | ||
export const wrap = fn => { | ||
if (fn === undefined) { | ||
return undefined; | ||
} | ||
assert(typeof fn === 'function'); | ||
const capture = __storage__; | ||
const wrapperFn = (...args) => { | ||
const prev = __storage__; | ||
try { | ||
__storage__ = capture; | ||
return fn(...args); | ||
} finally { | ||
__storage__ = prev; | ||
} | ||
}; | ||
return harden(wrapperFn); | ||
}; | ||
harden(wrap); |
43 changes: 43 additions & 0 deletions
43
packages/eventual-send/src/async-contexts/6-async-context-transpose.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
// eslint-disable-next-line no-underscore-dangle | ||
let __get__ = harden(_m => undefined); | ||
|
||
export const makeAsyncContext = () => { | ||
const transposedMap = new WeakMap(); | ||
|
||
return harden({ | ||
run: (val, cb, args = []) => { | ||
const prev = __get__; | ||
const key = harden({}); | ||
transposedMap.set(key, val); | ||
try { | ||
__get__ = harden(m => (m.has(key) ? m.get(key) : prev(m))); | ||
return cb(...args); | ||
} finally { | ||
__get__ = prev; | ||
} | ||
}, | ||
|
||
get: () => __get__(transposedMap), | ||
}); | ||
}; | ||
harden(makeAsyncContext); | ||
|
||
// Exposed only to the internal `then` function? | ||
export const wrap = fn => { | ||
if (fn === undefined) { | ||
return undefined; | ||
} | ||
assert(typeof fn === 'function'); | ||
const capture = __get__; | ||
const wrapperFn = (...args) => { | ||
const prev = __get__; | ||
try { | ||
__get__ = capture; | ||
return fn(...args); | ||
} finally { | ||
__get__ = prev; | ||
} | ||
}; | ||
return harden(wrapperFn); | ||
}; | ||
harden(wrap); |
13 changes: 13 additions & 0 deletions
13
packages/eventual-send/src/async-contexts/7-fluid-passing-style-transform.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
Imagine that all user code is rewritten according to the following fps (fluid passing style) transform, by analogy to a global cps transform. | ||
* Rewrite every pre-fps variable name by prepending an underbar. | ||
* For each pre-fps function definition, rewrite it to a post-fps function definition with an additional first `F` parameter. | ||
* For each pre-fps function call, rewrite it to a post-fps function call with an additional first `F` argument. | ||
* Do not transform `7-fluid-passing-style.js` itself. Rather, accept it as written manually in the post-fps language. | ||
|
||
```js | ||
(x, y) => f(a, b); | ||
``` | ||
to | ||
```js | ||
(F, _x, _y) => _f(F, _a, _b); | ||
``` |
34 changes: 34 additions & 0 deletions
34
packages/eventual-send/src/async-contexts/7-fluid-passing-style.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
// @ts-nocheck | ||
/* eslint-disable no-underscore-dangle */ | ||
// Given that the post-fps language has its own hidden | ||
// WeakMap, harden, undefined, assert | ||
|
||
// eslint-disable-next-line no-unused-vars | ||
export const _makeAsyncContext = F1 => { | ||
const transposedMap = new WeakMap(); | ||
|
||
return harden({ | ||
run: (F2, _val, _cb, _args = []) => { | ||
const key = harden({}); | ||
transposedMap.set(key, _val); | ||
const F3 = harden(m => (m.has(key) ? m.get(key) : F2(m))); | ||
return _cb(F3, ..._args); // No try! | ||
}, | ||
get: F4 => F4(transposedMap), | ||
}); | ||
}; | ||
harden(_makeAsyncContext); | ||
|
||
// Exposed only to the internal `then` function? | ||
export const _wrap = (F5, _fn) => { | ||
if (_fn === undefined) { | ||
return undefined; | ||
} | ||
assert(typeof _fn === 'function'); | ||
// eslint-disable-next-line no-unused-vars | ||
const _wrapper = (F6, ...args) => { | ||
return _fn(F5, ...args); | ||
}; | ||
return harden(_wrapper); | ||
}; | ||
harden(_wrap); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
A graduated sequence of example shim code, to better understand the security risks of the AsyncContext proposal to tc39. | ||
|
||
- [0-sync-context-original.js](./0-sync-context-original.js) is a class style non-transposed `Map`-based implementation of synchronous fluid binding, based on Justin's [Slide 6](https://docs.google.com/presentation/d/1yw4d0ca6v2Z2Vmrnac9E9XJFlC872LDQ4GFR17QdRzk/edit#slide=id.g198251ee25f_2_6). | ||
|
||
- [1-sync-context-maker.js](./1-sync-context-maker.js) is an ***objects-as-closures*** style non-transposed `Map`-based implementation of synchronous fluid binding. It is equivalent to [0-sync-context-original.js](./0-sync-context-original.js) except written in objects-as-closure style rather than class style. The externally visible differences are | ||
* The `makeSyncContext` function rather than the `SyncContext` class. | ||
* The resulting need to call the maker as a function, rather than using `new`. | ||
* The absence of anything analogous to a `SyncContext.prototype` object. | ||
* And the methods being own properties of the instance rather than inherited properties. | ||
|
||
For all of these differences, the class-style is more representative of the API one would actually propose and shim. We shift to objects-as-closure style only so that the semantics can be more easily understood. | ||
|
||
- [2-sync-context-shallow.js](./2-sync-context-shallow.js) is an equivalent ***shallow*** binding implementation, that should be observationally equivalent without using top-level state. Thereby showing the original is equally safe. This is closest to the classic model of fluid scoping. But it doesn't generalize to asynchronous fluid bindings since `wrap` would be impossible to implement. Hence our overall focus on deep-binding implementations. | ||
|
||
- [3-sync-context-deep.js](./3-sync-context-deep.js) is an equivalent deep-binding non-transposed ***`WeakMap`-based*** implementation. Shifting to `WeakMap` enables us to transpose. | ||
|
||
- [4-sync-context-transpose.js](./4-sync-context-transpose.js) is an equivalent deep-binding ***transposed*** `WeakMap`-based implementation. By transposing, we can remove all mutable state rooted in the problematic global mutable variable, moving that mutable state into the `SyncContext` instances, regaining some of the safe look of [2-sync-context-shallow.js](2-sync-context-shallow.js). | ||
|
||
- [5-async-context-original.js](./5-async-context-original.js) is a deep-binding non-transposed `Map`-based implementation of ***asynchronous*** fluid binding, based on Justin's [Slide 11](https://docs.google.com/presentation/d/1yw4d0ca6v2Z2Vmrnac9E9XJFlC872LDQ4GFR17QdRzk/edit#slide=id.g18e6eaa50e1_0_192) and [Slide 13](https://docs.google.com/presentation/d/1yw4d0ca6v2Z2Vmrnac9E9XJFlC872LDQ4GFR17QdRzk/edit#slide=id.g191c1f7e99f_0_0). It is identical to [0-sync-context-original.js](./0-sync-context-original.js) but for the addition of a `wrap` function. | ||
|
||
- [6-async-context-transpose.js](./6-async-context-transpose.js) is an equivalent deep-binding ***transposed*** `WeakMap`-based implementation of ***asynchronous*** fluid binding. It is identical to [4-sync-context-transpose.js](./4-sync-context-transpose.js) but for the addition of that `wrap` function. `wrap` still manipulates an encapsulated top-level mutable variable, but each value bound to this varible is transitively immutable and powerless. | ||
|
||
- [test-attack.js](../../test/async-contexts/test-attack.js) demonstates an attack, in which Carol creates a separate Alice and Bob that are confined and isolated from each other. They can communicate *only* as mediated by Carol. Carol introduces them *only* by giving each a new no-argument closure of Carol's creation, each of which logs what it does: invoke a method of the counterparty with no arguments. The two test cases show that Carol has in fact enabled Alice to communicate a secret to Bob even though the log is the same, and therefore independent of the secret. This would not be possible in Hardened JavaScript without `wrap`. | ||
|
||
- [test-reveal-defense.js](../../test/async-contexts/test-reveal-defense.js) shows how Carol can use `AsyncContext` to defend against this attack by revealing the difference in what Alice communicates to Bob, so that Carol can tell that Bob might also detect a difference. [test-censor-defense.js](../../test/async-contexts/test-censor-defense.js) show how Carol can use `AsyncContext` to defend against this attack by preventing Alice from using `AsyncContext` to communicate indirectly with Bob. | ||
|
||
- [test-async-attack.js](../../test/async-contexts/test-async-attack.js), [test-async-reveal-defense.js](../../test/async-contexts/test-async-reveal-defense.js), and [test-async-censor-defense.js](../../test/async-contexts/test-async-censor-defense.js) are corresponding attack and defense examples under the assumption that `wrap` is not publicly available, but rather is used (to explain the behavior of) the builtin `then` function, and thereby the original `Promise.prototype.then` method and the `await` syntax. The parallelism of attack and defense says that exposing `wrap` does not cause any significant additional loss of security. | ||
|
||
--- | ||
|
||
Without `wrap`, [6-async-context-transpose.js](./6-async-context-transpose.js) would still be observationally equivalent to [2-sync-context-shallow.js](2-sync-context-shallow.js), and therefore obviously safe. The top-level state manupulatd by `wrap` is still encapsulated. We assume that `wrap` itself is not exposed, but rather only used to explain the needed changes to the semantics of the internal `then` function. The internal `then` function explains the behavior of both the primordial `then` method and the `await` syntax, so its ability to manipulate top-level state via `wrap` remains worrisome. | ||
|
||
But notice that we already live with a similar hazard that we've come to realize is safe: The internal `then` function (and therefore the primordial `then` and the `await` syntax) already manipulate other top-level state that is not otherwise reachable: The job queue. It is in fact weird that no capability to the job queue is required to schedule jobs on this mutable job queue. Ambient access to *the one top level mutable* job queue does have some downsides that would be absent if we did not allow this exception: | ||
|
||
Say a hostile subgraph is fully encapsulated in a revocable membrane. Once the membrane is revoked, the hostile subgraph can no longer cause any effects on the world outside itself. [An omniscient garbage collector could therefore collect it](https://www.youtube.com/watch?v=oBqeDYETXME&list=PLKr-mvz8uvUgybLg53lgXSeLOp4BiwvB2&index=5&t=1574s), knowing that this collection would be unobservable. In the absence of this job-queue exception, that would also be true for actual garbage collectors. However, by using the power of the internal `then` function, the disconnected subgraph can keep rescheduling itself, and so remain resident, continuing to use both space and time resources. Had access to the job queue been mediated by a capability, access to the actual capability would have been severed by the membrane, so actual collectors could then sweep up the garbage. This demonstrates that the ambient internal `then` weakens availability. | ||
|
||
But the ambient internal `then` does not seem to endanger integrity, nor ***overt*** confidentiality --- leakage of info over [overt channels](https://agoric.com/blog/all/taxonomy-of-security-issues/). The key seems to be that computation isolated from all the objects in turn T1 cannot overtly observe any of the turns spawned by turn T1. It is at least plausible that the `AsyncContext` proposal, being observationally equivalent to [6-async-context-transpose.js](./6-async-context-transpose.js) is equally safe. The only difference from the obviously-safe [2-sync-context-shallow.js](2-sync-context-shallow.js) is the extension of context from a spawned turn T1 to the turns it spawns. The state associated with an `AsyncContext` instance is unobservable to | ||
* anything that cannot run in the spawned turn (by the safety properties of the existing `then`), | ||
* anything without access to that `AsyncContext` instance (by the safety demonstrated by [2-sync-context-shallow.js](2-sync-context-shallow.js)). |
26 changes: 26 additions & 0 deletions
26
packages/eventual-send/test/async-contexts/async-attack-tools.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { makePromiseKit, makeQueue, ResolveThen } from './async-tools.js'; | ||
|
||
export const makeCallAsyncInCurrentContext = () => { | ||
const { resolve, promise } = makePromiseKit(); | ||
const result = ResolveThen(promise, cb => cb()); | ||
return harden(cb => { | ||
resolve(cb); | ||
return result; | ||
}); | ||
}; | ||
harden(makeCallAsyncInCurrentContext); | ||
|
||
export const makeStickyContextCaller = () => { | ||
const invocationQueue = makeQueue(); | ||
invocationQueue.put(makeCallAsyncInCurrentContext()); | ||
|
||
return cb => | ||
ResolveThen(invocationQueue.get(), call => | ||
call(() => { | ||
// This is running in the sticky initial context | ||
invocationQueue.put(makeCallAsyncInCurrentContext()); | ||
return cb(); | ||
}), | ||
); | ||
}; | ||
harden(makeStickyContextCaller); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { wrap } from '../../src/async-contexts/6-async-context-transpose.js'; | ||
|
||
export const makePromiseKit = () => { | ||
let resolve; | ||
let reject; | ||
const promise = new Promise((res, rej) => { | ||
resolve = res; | ||
reject = rej; | ||
}); | ||
return harden({ promise, resolve, reject }); | ||
}; | ||
harden(makePromiseKit); | ||
|
||
export const ResolveThen = (value, onFulfilled = val => val, onRejected) => { | ||
const { promise, resolve } = makePromiseKit(); | ||
resolve(value); | ||
return promise.then(wrap(onFulfilled), onRejected ?? wrap(onRejected)); | ||
}; | ||
harden(ResolveThen); | ||
|
||
const { freeze } = Object; | ||
|
||
export const makeQueue = () => { | ||
let { promise: tailPromise, resolve: tailResolve } = makePromiseKit(); | ||
return { | ||
put(value) { | ||
const { resolve, promise } = makePromiseKit(); | ||
tailResolve(freeze({ value, promise })); | ||
tailResolve = resolve; | ||
}, | ||
get() { | ||
const promise = ResolveThen(tailPromise, next => next.value); | ||
tailPromise = ResolveThen(tailPromise, next => next.promise); | ||
return harden(promise); | ||
}, | ||
}; | ||
}; | ||
harden(makeQueue); |
Oops, something went wrong.