Skip to content

Commit

Permalink
feat(AsyncContext): examine security of AsyncContext
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Jan 14, 2023
1 parent ccd003c commit 0735be9
Show file tree
Hide file tree
Showing 18 changed files with 1,017 additions and 0 deletions.
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 packages/eventual-send/src/async-contexts/1-sync-context-maker.js
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);
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 packages/eventual-send/src/async-contexts/3-sync-context-deep.js
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);
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);
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);
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);
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 packages/eventual-send/src/async-contexts/7-fluid-passing-style.js
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);
39 changes: 39 additions & 0 deletions packages/eventual-send/src/async-contexts/README.md
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 packages/eventual-send/test/async-contexts/async-attack-tools.js
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);
38 changes: 38 additions & 0 deletions packages/eventual-send/test/async-contexts/async-tools.js
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);
Loading

0 comments on commit 0735be9

Please sign in to comment.