-
Notifications
You must be signed in to change notification settings - Fork 165
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
Bulletproofing the async requirement #139
Comments
First, my apologies for letting my attention to this issue slide. As it became a long thread, I kept thinking I needed to catch up and then contribute. I never found the time to do that. So I'll take this opportunity to respond to this reboot even though I lack some of the previous context. My argument of course is against "This should be allowed to call a then b then c:" Under normal event loop assumptions, the following is a perfectly sensible bit of defensive code, where state is encapsulated and api is exposed to untrusted clients:
What I mean here by "mildly" is that api on entry assumes that state's invariants may already be mildly suspended, i.e., the suspension of invariants is carefully designed to tolerate recursive entry at the callback() point. Otherwise it would not be safe to call callback synchronously. This might seem a bit contrived, but I know I've done exactly this and can probably dig up some examples. The second part of our api method does a different defensive pattern. It doesn't know what p is, but it knows that Q(p) is a genuine promise, and so it knows that Q(p).then will call its argument function in a fresh turn, where all of state's invariants have been restored. Under the looser requirements proposed above, this assumption can be violated as follows:
This program does not even try to attack by recursive entry, since api was designed to defend against that. The first call to api suspends and restores state's invariants no problem. It also schedules something1 to happen later, after p is allegedly settled (i.e., after Q(p) is settled), where that something1 relies on state's invariants being intact. The second call to api happens during a later turn. This suspends state's invariants again and then calls the callback. This callback fulfills p, causing something1 to potentially run now, while state's invariants are suspended, violating something1's assumptions. Then the callback returns, restoring the invariants and scheduling something2 to happen in a later turn. In that later turn, something2 runs fine. The problem was exactly that something1 happened while api was on the stack with suspended state invariants. |
I'd like to repeat my point from #104 that the behavior we're argumenting about relies heavily on the resolver spec. I'll extend my proposal to specify the behavior of
A library may follow the spec to a certain level of strictness, or it might provide options or different resolver functions for less strict behaviour than (0). |
Wouldn't all three of these options still be vulnerable to this attack? |
@erights: Only if OK, maybe my wording for levels 0 and 1 needs improvement to forbid something like
|
How about
? |
@bergus: I'd like to clear this once and for all. We cannot leave this to a hypothetical resolvers spec. It is imperative that, when someone receives a Promises/A+ promise, they have predictable behavior in case (Resolve-After-Then) and similar. Promises/A+ specifies many things about the behavior of resolution, even if it doesn't specify what exact syntax is used for performing resolution. In particular, it specifies all things about the behavior of the @erights: thanks for the clear example. This may sink (Resolve-Inside-Async), and thus make our job a lot easier in terms of what to specify. But it would really be a shame, because it means promise libraries will always be behind callback libraries in performance. For example, code such as function readFile(fileName) {
return new Promise((resolve, reject) => {
fs.readFile(fileName, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
}
readFile("src.txt").then(...) will always be penalized by an extra tick in comparison to simply calling Can you think of any way around this? Or do we just have to swallow our pride and have promises always be slower than callbacks, in exchange for avoiding that attack you mention? |
@domenic Looking at your example makes even clearer to me that we need to swallow. When
is called from deep within a turn, it is important for the programmer to know that the .then callbacks will only be called later, in their own turn. If the programmer actually wants to waive this temporal isolation, it is good that they'd have to write
explicitly, so that this absence of temporal isolation is clear. |
Hmm, I think I may have miscommunicated. Because the underlying It's the additional turn, beyond the ones already passed by going out to the hard disk, that I would prefer to avoid if we could. |
@domenic As for the performance cost, keep in mind that this will all quickly turn into mechanism directly supported by the browsers, and soon after, by JS engines. This won't make it efficient in the short term but will in the longer term. |
@domenic I see. Actually, in context your example is clear and I jumped to the wrong conclusion. Thanks for the clarification. Nevertheless, I don't think this extra tick is avoidable. I still think we need to swallow. (Once promises move into the JS engine, I have some ideas about how such things might be optimized, but I'll leave those speculations for another day.) |
If others are agreed, i.e. that (Resolve-Inside-Async) should always be a-c-b, then I think we can take two approaches: Take 1
Footnote: Here "platform code" means engine, environment, and promise implementation code. In practice, this requirement ensures that Take 2
Footnote: In practical terms, an implementation must use a mechanism such as |
What operational difference might there be between these two? Would take 2 allow a "promise implementation ... may itself contain a task-scheduling queue or "trampoline" in which the handlers are called."? If not, then I am prefer take 1. |
When I wrote this 11 hours ago, I thought there was no operational difference between the two, and it was just a matter of which concepts we find more relevant or less confusing. But now I think take 2 is indeed a failure; it breaks trampolining, i.e. (Then-Inside-Then), and it allows a-b-c for (Resolve-Inside-Async), which we just spent a couple days establishing as bad. Oops! Not sure what I was thinking. In that case I think, modulo anyone else chiming in, we should settle on take 1. |
I can live with Take 1, but it should provide an example of how |
@briancavalier would love your thoughts. |
Sorry guys, things have been very hectic on my end for the past two weeks. It does seem like Take 1 is a good way to go. There might be some gray areas around "platform code", even though you've specified what that means. I think we can probably just deal with such gray areas as they arise. |
even if this issue is old and closed, could you please edit top comment for Resolve-Inside-Async case? |
Previous discussions in #84 (comment), #70, and #100.
Requirements
All of these must call
a
, thenb
, thenc
.This should be allowed to call
a
thenb
thenc
:Naive implementations would probably do
a
thenc
thenb
, but because we are inside asetImmediate
, it is at least one turn afterp.then(b)
occured, so it should be OK forresolve()
to callb
synchronously, in betweena
andc
.The Situation
Currently, the spec's wording is:
This is correct for (Basic), (Resolve-Before-Then), (Then-Inside-Then), and (Resolve-Inside-Async), but incorrect for (Resolve-After-Then).
The Goal
Can anyone figure out phrasing that will be correct for all cases?
The text was updated successfully, but these errors were encountered: