-
Notifications
You must be signed in to change notification settings - Fork 81
POC broken alt cancellation 3032 #3035
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: kriskowal-cancel
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,67 @@ | |
| * @import { Cancelled, Cancel, CancelKit } from './types.js' | ||
| */ | ||
|
|
||
| const { apply } = Reflect; | ||
|
|
||
| /** | ||
| * @type {WeakSet<Promise>} | ||
| */ | ||
| const knownCancelledSet = new WeakSet(); | ||
| harden(knownCancelledSet); | ||
|
|
||
| const thenMethod = Promise.prototype.then; | ||
|
|
||
| // TODO more accurate typing of `when`. It is intended to emulate E.when. | ||
| /** | ||
| * Since this package has no dependencies, it likely needs to work reliably in | ||
| * a non-ses environment too. | ||
| * | ||
| * @template {any} V | ||
| * @template {any} T | ||
| * @param {Promise<V>} promise | ||
| * @param {(v: V) => T} [onFulfilled] | ||
| * @param {(reason: Error) => T } [onRejected] | ||
| * @returns {Promise<T>} | ||
| */ | ||
| const when = (promise, onFulfilled = undefined, onRejected = undefined) => | ||
| apply(thenMethod, promise, [onFulfilled, onRejected]); | ||
|
|
||
| /** | ||
| * Says whether a promise, if interpreted as a cancellation token, is known | ||
| * to be cancelled. If the promise was made by the CancelKit, the answer is | ||
| * always immediately accurate. | ||
| * | ||
| * For other promises, we interpret any rejected state as cancelled. | ||
| * For these, the answer is false the first time, since we do not yet know. | ||
| * But once the promise settles, we will eventually | ||
| * have an accurate answer. Thus, if the promise is well behaved, | ||
| * once the answer is ever `true` it should remain `true` forever. | ||
| * A passable promise is necessarily well behaved, so for those, this | ||
| * monotonicity is guaranteed. | ||
| * | ||
| * XXX SECURITY BUG DO NOT MERGE THIS EXPERIMENT | ||
| * | ||
| * The fact that this always says false the first time we ask about an | ||
| * unrelated promise is a global communications channel. | ||
| * | ||
| * @param {Promise} promise | ||
| * @returns {boolean} | ||
| */ | ||
| export const isKnownCancelled = promise => { | ||
| if (knownCancelledSet.has(promise)) { | ||
| return true; | ||
| } | ||
| when( | ||
| promise, | ||
| _v => {}, | ||
| _reason => { | ||
| knownCancelledSet.add(promise); | ||
| }, | ||
| ); | ||
| return false; | ||
| }; | ||
| harden(isKnownCancelled); | ||
|
|
||
| /** | ||
| * Creates a cancellation kit containing a cancellation token and a cancel function. | ||
| * | ||
|
|
@@ -20,47 +81,39 @@ | |
| * @param {Cancelled} [parentCancelled] - Optional parent cancellation token | ||
| * @returns {CancelKit} | ||
| */ | ||
| export const makeCancelKit = parentCancelled => { | ||
| /** @type {undefined | true} */ | ||
| let cancelledState; | ||
|
|
||
| export const makeCancelKit = (parentCancelled = undefined) => { | ||
| /** @type {Cancel | undefined} */ | ||
| let internalReject; | ||
|
|
||
| const promise = new Promise((_resolve, reject) => { | ||
| /** @type {Cancelled} */ | ||
| const cancelled = new Promise((_resolve, reject) => { | ||
| internalReject = reject; | ||
| }); | ||
|
|
||
| // Prevent unhandled rejection warnings when the promise is not awaited | ||
| promise.catch(() => {}); | ||
|
|
||
| // Define the cancelled getter on the promise | ||
| Object.defineProperty(promise, 'cancelled', { | ||
| get() { | ||
| return cancelledState; | ||
| }, | ||
| enumerable: false, | ||
| configurable: false, | ||
| }); | ||
| // Propagate cancellation from parent if provided | ||
| if (parentCancelled) { | ||
| if (isKnownCancelled(parentCancelled)) { | ||
| knownCancelledSet.add(cancelled); | ||
| } | ||
| } | ||
|
Comment on lines
+93
to
+98
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an orthogonal observable change from #3032 . I claim it is an improvement, in that if the parent is already known to be cancelled, then the child is born cancelled. You could adopt this observable change by itself, without the rest of the PR. Btw, this observable change did not affect any tests. |
||
|
|
||
| /** @type {Cancelled} */ | ||
| const cancelled = /** @type {Cancelled} */ (promise); | ||
| // Prevent unhandled rejection warnings when the promise is not awaited | ||
| when(cancelled, undefined, () => {}); | ||
|
|
||
| /** @type {Cancel} */ | ||
| const cancel = reason => { | ||
| if (cancelledState === undefined) { | ||
| cancelledState = true; | ||
| const error = reason || Error('Cancelled'); | ||
| if (internalReject) { | ||
| internalReject(error); | ||
| internalReject = undefined; | ||
| } | ||
| const error = reason || Error('Cancelled'); | ||
| if (internalReject) { | ||
| internalReject(error); | ||
| internalReject = undefined; | ||
| knownCancelledSet.add(cancelled); | ||
| } | ||
| }; | ||
|
|
||
| // Propagate cancellation from parent if provided | ||
| if (parentCancelled) { | ||
| parentCancelled.then( | ||
| when( | ||
| parentCancelled, | ||
| () => {}, | ||
| reason => cancel(reason), | ||
| ); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Turns out that
isKnownCancelled's use of this piece of static mutable state does make it a communication channel. This SECURITY BUG seems inherent in this approach, which is why this is a failed experiment. See below.