Closed
Bug 1331662
Opened 9 years ago
Closed 9 years ago
nsJSUtils::EvaluateString does too many things.
Categories
(Core :: General, defect)
Core
General
Tracking
()
RESOLVED
FIXED
mozilla55
| Tracking | Status | |
|---|---|---|
| firefox55 | --- | fixed |
People
(Reporter: nbp, Assigned: nbp)
References
Details
Attachments
(3 files, 3 obsolete files)
|
30.16 KB,
patch
|
Details | Diff | Splinter Review | |
|
13.32 KB,
patch
|
bzbarsky
:
review+
|
Details | Diff | Splinter Review |
|
16.97 KB,
patch
|
bzbarsky
:
review+
|
Details | Diff | Splinter Review |
Currently nsJSUtils::EvaluateScript handles a lot of cases, and it handles them inconsitently between all of them. Bug 900784 prototype patches adds even more complexity by adding an extra JSScript out-param to the function, which does not behave consistently either (Bug 900784 comment 50 and Bug 900784 comment 53).
Another inconsistency is related to the usage of off-thread parsing, while running with options which expects to have a return value. I guess this case never happens in practice, because the returned value is never set when using off-thread-parsing.
One of the reason which is likely the root cause of this, is the need to avoid code duplication for the setup of the compartment.
I think this problem would be better solved by using lambdas or RAII.
| Assignee | ||
Comment 1•9 years ago
|
||
I am only asking for feedback, because I am currently hunting a bug from
this patch which cause Firefox to only show the URL bar, with a
non-actionable hamburger button and a blank page.
Otherwise, this patch should highlight the direction I took so far, which is
to split the EvaluateString function into SyncAndExec, CompileAndExec, and
InCompartment.
The signature of the function is made such that one cannot miss-use them, as
InCompartment is the only function capable of creating the CompartmentScope
reference expected as argument of SyncAndExec, and CompileAndExec.
I noticed that EvaluateString was used in 2 different contextes.
1. This is a top-level script, and we do not expect any return value, and
the scopechain argument is empty.
2. This is a function evaluated in the context of the scope chain of
another function, such as XBL and plugins.
So, I named them accordingly, (1) being the TopLevel context and (2) being
the Embedded context.
InCompartment is overloaded to handle (1) and (1 & 2) cases, where the (1 &
2) case rely on the (1) case, by adding the return value management.
One detail of implementation is the use of std::function<>, and in
particular the way the argument is provided. std::function<> are by default
allocating space to copy the function content. Lambda are not lightweight
objects, and I would prefer to avoid allocations that cannot be checked.
std::function<> constructor has one particular implementation detail about
function pointers and std::reference_wrapper [1], and thus to avoid allocations
we have to explictly take a reference_wrapper of the lambdas.
[1] http://en.cppreference.com/w/cpp/utility/functional/function/function
Attachment #8828853 -
Flags: feedback?(jcoppeard)
Attachment #8828853 -
Flags: feedback?(bkelly)
| Assignee | ||
Comment 2•9 years ago
|
||
Comment on attachment 8828853 [details] [diff] [review]
[lambda] Replace nsJSUtils::EvaluateString by SyncAndExec, CompileAndExec and InCompartment functions.
Review of attachment 8828853 [details] [diff] [review]:
-----------------------------------------------------------------
Ok, I am now able to run the browser successfully with the following self-nits applied.
(feedback -> review)
::: dom/base/nsJSUtils.cpp
@@ +176,2 @@
>
> + auto topLevelSwitch = [&](CompartmentScope& aCs) {
self-nit: We have to set the return value in CompileAndExec, thus we should set aCs.embedded as follow:
aCs.embedded = &aEmbedded;
@@ +197,3 @@
> JSString* str = JS::ToString(aCx, value);
> + if (!!str) {
> + aEmbedded.retValue.set(JS::UndefinedValue());
self-nit: Inverted condition.
Attachment #8828853 -
Flags: review?(jcoppeard)
Attachment #8828853 -
Flags: review?(bkelly)
Attachment #8828853 -
Flags: feedback?(jcoppeard)
Attachment #8828853 -
Flags: feedback?(bkelly)
Comment 3•9 years ago
|
||
Comment on attachment 8828853 [details] [diff] [review]
[lambda] Replace nsJSUtils::EvaluateString by SyncAndExec, CompileAndExec and InCompartment functions.
Redirecting to Boris since my knowledge of js api is marginal at best.
Attachment #8828853 -
Flags: review?(bkelly) → review?(bzbarsky)
Comment 4•9 years ago
|
||
Sorry for the lag here... I've been trying to understand the new setup and why it's better. It trades off having all the code be in one place (but branchy and complicated) for having the code scattered among multiple callbacks and very much nonlocal, and imo harder to reason about.
Why do we need to expose this complexity in the API? In API terms, there are really only a few things people do here. We should try to make those clear and understandable, with more or less one public API function per task; then we can figure out how to improve the implementation to be less branchy/complicated and still support that API, right?
I have no problem with renaming the EvaluateString that takes a script token to something else, obviously. I agree that having it named EvaluateString is confusing, since that's not at all what it does.
Flags: needinfo?(nicolas.b.pierron)
| Assignee | ||
Comment 5•9 years ago
|
||
The problem I faced, is that with Bug 900784, I was either adding more branches or duplicating code[1]. I found even more confusing and harder to maintain, from a security point of view.
In comparison, this work allow me to instead add a SyncEncodeAndExec and a DecodeAndExec[2] which are first better named, and then shorter, and thus less likely to be buggy nor to introduce bug in nearby code.
One of the problem I have with EvaluateString, is the fact that it takes so many argument that we lose track[3] of what is related to what. This patch adds dedicated functions, which do loading & eval logic and can be customize without adding even more arguments to the EvaluateString functions, nor having to change every caller[4] when another unrealted argument is added.
In comparison the InCompartment function is overloaded twice, depending if we want to evaluate the code in the context of the global, at the top-level, or in the context of an inner function like many other uses cases.
I think one of the issue here is that the lambda is not given directly as argument to the function, and this C++ limitation(5) makes things harder to read. If I were to modify the code to male the callers look like the following be preferable from your point of view?
rv = nsJSUtils::InCompartment(cx, topLevelCtx, [&](nsJSUtils::CompartmentScope& aCs) {
return SyncAndExec(aCs, …);
});
Otherwise, what kind of API are you thinking of? Is the complexity coming from the TopLevelContext and EmbeddedContext classes, in which case we can remove them by adding extra arguments to the InCompartment functions. What do you tihnk?
I honestly think that the example above is clear and easy to follow. It also removes unused/useless code in the context of the nsScriptLoader and should use less stack space. I would assume that the CPU prefetcher should be able to follow function pointers given as argument, solving the locality issue.
[1] (Bug 900784) attachment 8800644 [details] [diff] [review]
[2] https://github.com/nbp/gecko-dev/commit/c1d086ef9442e28bc1e201da2865a4c37e7b8a73?diff=split#diff-ae87530e1e8b54e2f265b7c1dceff482R299
[3] https://bugzilla.mozilla.org/show_bug.cgi?id=900784#c53
[4] https://bugzilla.mozilla.org/show_bug.cgi?id=900784#c51
(5) C++ has no way to give a lambda as argument to a function without allocating with an std::function, or using templates.
Flags: needinfo?(nicolas.b.pierron) → needinfo?(bzbarsky)
Comment 6•9 years ago
|
||
I understand your concerns with the "private" EvaluateString function, which does in fact have too many arguments.
My concern is with the "public" API. The public API basically consists of a very small number of operations (not necessarily well reflected in our current public API!):
1) Evaluate the given string (always nsAString; the JS::SourceBufferHolder version
looks unused to me) against the given global, with the given compile options
and given scope chain, and return the resulting value. One of the callers here has an empty scope
chain (and is the only consumer of setCoerceToString) the others have nonempty scope
chains. So maybe this should actually be two separate APIs: one with scope chain and
no coercion, one without scope chain but with coercion.
2) Evaluate the given string (nsAString or JS::SourceBufferHolder) with the given
compile options and given global (implies empty scope chain, no return value needed).
3) Take the given off-thread compilation token, finish the script, and execute it
against the given global. I guess we do in fact need the global here,
because JS::FinishOffThreadScript wants to be done in the right compartment.
4) Whatever new thing(s) you are adding.
Agreed on that? If we agree on that, then I think we should make the public API actually reflect those tasks. Then we can implement it internally with RAII, and lambdas if needed and whatnot, but at that point the lambdas would all be self-contained in nsJSUtils and much simpler to reason about, imo.
If you think this isn't very workable, maybe the problem is that I don't have a good handle on the new public API you want to add...
> and can be customize without adding even more arguments to the EvaluateString
> functions, nor having to change every caller[4]
Maybe I'm missing something, but I don't see the thing linked to at [4] having to change every caller... In fact, it doesn't mention nsJSUtils at all.
> Is the complexity coming from the TopLevelContext and EmbeddedContext classes
I think for me the complexity comes from it just being hard to follow what the logic flow is and not having a good idea of what the invariants are. By the time the lambda is executed, what work has already happened? What work is the lambda responsible for?
It doesn't help (and this is a problem before your patches too!) that my public API #3 really doesn't need a bunch of the stuff we pass in for all the other cases, and in particular doesn't need a CompileOptions. But I suspect neither do your two new cases, right? Forcing all TopLevelContext consumers to produce a CompileOptions isn't all that great. Ideally, it would only be passed to my cases 1-3 above, and the CompileOptions-related assert would move out of the shared "set up the compartment" code and closer to the actual compilation...
Flags: needinfo?(bzbarsky)
| Assignee | ||
Comment 7•9 years ago
|
||
Summary of the IRC discussion with bz:
We are moving away from lambda implementation toward a RAII implementation. Thus, replacing
rv = nsJSUtils::InCompartment(cx, global, [&](nsJSUtils::CompartmentScope& aCs) {
return SyncAndExec(aCs, …);
});
by
{
nsJSUtils::ExecuteContext execCtx(cx, global);
rv = nsJSUtils::SyncAndExec(execCtx, …);
}
| Assignee | ||
Updated•9 years ago
|
Attachment #8828853 -
Attachment description: Replace nsJSUtils::EvaluateString by SyncAndExec, CompileAndExec and InCompartment functions. → [lambda] Replace nsJSUtils::EvaluateString by SyncAndExec, CompileAndExec and InCompartment functions.
Attachment #8828853 -
Flags: review?(jcoppeard)
Attachment #8828853 -
Flags: review?(bzbarsky)
| Assignee | ||
Comment 8•9 years ago
|
||
This patch re-do the same modification using RAII.
One of the problem which arised was the need for the enterCompartment
function. The enterCompartment function is used to do the same early return
that is made if xpc::Scriptability::Get(aGlobal).Allowed() fails.
I do not know if this check is needed for all the caller, or if it can be
moved outside only one of the caller.
As I added an enterCompartment, I named the other function
exitCompartment.
Sadly, the code to be made at each call-site is quite verbose.
Attachment #8832488 -
Flags: feedback?(bzbarsky)
| Assignee | ||
Comment 9•9 years ago
|
||
Comment on attachment 8832488 [details] [diff] [review]
Replace nsJSUtils::EvaluateString by an ExecutionContext class with different compile and execute functions.
Review of attachment 8832488 [details] [diff] [review]:
-----------------------------------------------------------------
::: dom/base/nsJSUtils.cpp
@@ +156,5 @@
> + : ExecutionContext(aCx)
> +{
> + MOZ_ASSERT_IF(aCoerceToString, !aCompileOptions.noScriptRval);
> + mHasReturnValue_ = !aCompileOptions.noScriptRval;
> + mCoerceToString_ = aCoerceToString;
I think it would be better to have a
exec.setExpectReturnValue()
.setCoerceToString(true)
Instead of the boolean argument on the constructor.
Having a
using CoerceToString = bool;
in the class is quite verbose, as it would be used as:
nsJSUtils::ExecutionContext::CoerceToString(true)
::: dom/base/nsJSUtils.h
@@ +64,5 @@
> const nsAString& aBody,
> JSObject** aFunctionObject);
>
> + // ExecutionContext is used to switch compartment.
> + class MOZ_STACK_CLASS ExecutionContext {
I think this name is terrible, I am open to better suggestions.
| Assignee | ||
Comment 10•9 years ago
|
||
Comment on attachment 8832488 [details] [diff] [review]
Replace nsJSUtils::EvaluateString by an ExecutionContext class with different compile and execute functions.
I will note that this patch does not work, because I am using JSErrorToNSResult, but I did not pay attention that the returned value are *NS_SUCCESS*_failure_case, which does not work as I would have expected when flowing in NS_FAILED.
I am trying to re-make this patch with more incremental modification to avoid such mistakes.
| Assignee | ||
Comment 11•9 years ago
|
||
This patch add all ExecutionContext methods, and re-implement the current
EvaluateString function with the ExecutionContext functions.
This implementation does not attempt to be clever as the opposite one, and
keeps every variable which was present before as member of the
ExecutionContext.
I think we can improve on the mOk_ and mRv_ states, but I would leave this
task for a follow-up patch.
Attachment #8832963 -
Flags: review?(bzbarsky)
| Assignee | ||
Updated•9 years ago
|
Attachment #8832488 -
Attachment is obsolete: true
Attachment #8832488 -
Flags: feedback?(bzbarsky)
| Assignee | ||
Comment 12•9 years ago
|
||
This part moves the implementation of the EvaluateString function to all the
callers.
I use the do-break-while(false) patterns as a way to avoid adding more and more
levels of indentation, which would latter matter for upcoming
nsScriptLoader.cpp patches (Bug 900784)
Attachment #8832964 -
Flags: review?(bzbarsky)
Comment 13•9 years ago
|
||
OK, so looking at the resulting API, I'd like to propose a few changes:
1) Have ExecutionContext have the compartment semantics of JSAutoCompartment, so it just enters the compartment in its ctor and leaves in its dtor.
2) Just store the value of xpc::Scriptability::Get(aGlobal).Allowed() in the ctor, then check it in the various methods and just don't do any work if not allowed. The actual methods would return nsresult if they failed.
3) Replace the one-arg version of exitCompartment() with an extractReturnValue() or something along those lines.
4) Just infer the return value thing in compileAndExec, since we have a CompileOptions there anyway.
With those changes, the use in nsScriptLoader.cpp will look like this:
nsJSUtils::ExecutionContext exec(aes.cx(), global);
if (aRequest->mOffThreadToken) {
rv = exec.SyncAndExec(&aRequest->mOffThreadToken, &script);
} else {
nsAutoString inlineData;
SourceBufferHolder srcBuf = GetScriptSource(aRequest, inlineData);
rv = exec.CompileAndExec(options, srcBuf);
}
and the callsite in nsXBLProtoImplField.cpp will look like this:
nsJSUtils::ExecutionContext exec(cx, scopeObject);
rv = exec.CompileAndExec(options, nsDependentString(mFieldText,
mFieldTextLength));
if (NS_FAILED(rv)) {
return rv;
}
rv = exec.ExtractReturnValue(&result);
where extractReturnValue would make sure to never clobber an existing NS_SUCCESS_DOM_SCRIPT_EVALUATION_THREW (which it can do, because it knows what compileAndExec returned), etc.
How does that sound?
As a general other comment, please no trailing '_' on member names, and method names start with uppercase chars.
Comment 14•9 years ago
|
||
Comment on attachment 8832963 [details] [diff] [review]
part 1 - Reimplement EvaluateString using the ExecutionContext class.
Pending updated patches.
Attachment #8832963 -
Flags: review?(bzbarsky)
Updated•9 years ago
|
Attachment #8832964 -
Flags: review?(bzbarsky)
| Assignee | ||
Comment 15•9 years ago
|
||
Delta:
- Follow comment 13 & irc recommendation.
- Rename m*_ to m*.
- Capitalize member function names.
- Rename mOk_ to mSkip.
- Replace Maybe<JSAutoCompartment> by JSAutoCompartment, and initialize it
during the ExecutionContext constructor.
- Move the JS_WrapValue out of the ExtractReturnValue (and forgot to add it
back in this patch, but they should be present in part 2)
- forward the mRv result to be returned from ExtractReturnValue if mSkip is set.
Attachment #8834889 -
Flags: review?(bzbarsky)
| Assignee | ||
Updated•9 years ago
|
Attachment #8832963 -
Attachment is obsolete: true
| Assignee | ||
Comment 16•9 years ago
|
||
Delta:
- Follow part 1 modifications, and add JS_WrapValue in all cases which are
expecting a returned value.
I know I could remove some of the JS_WrapValue, but I prefer to keep these
changes mechanical for the moment. Maybe I should make a part 3 to replace
these by assertSameCompartment.
Attachment #8834892 -
Flags: review?(bzbarsky)
| Assignee | ||
Updated•9 years ago
|
Attachment #8832964 -
Attachment is obsolete: true
Comment 17•9 years ago
|
||
Comment on attachment 8834889 [details] [diff] [review]
part 1 - Reimplement EvaluateString using the ExecutionContext class.
>+++ b/dom/base/nsJSUtils.cpp
>+ PROFILER_LABEL("nsJSUtils", "ExecutionContext",
This isn't quite right: PROFILER_LABEL sets up an RAII class (SamplerStackFrameRAII) using its args. That used to measure the entire evaluation, but now will just measure the ExecutionContext ctor...
We probably want to make the SamplerStackFrameRAII a member instead.
>+nsJSUtils::ExecutionContext::SyncAndExec(void **aOffThreadToken,
>+ if (!aScript) {
(stuff)
>+ if (!JS_ExecuteScript(mCx, mScopeChain, aScript)) {
(same stuff)
I'd slightly prefer we do:
if (!aScript || !JS_ExecuteScript(mCx, mScopeChain, aScript)) {
mSkip = true;
mRv = EvaluationExceptionToNSResult(mCx);
return mRv;
}
but either way.
>+nsJSUtils::ExecutionContext::ExtractReturnValue(JS::MutableHandle<JS::Value> aRetValue)
>+ if (mHasReturnValue) {
I can't think of any sane reason someone would call ExtractReturnValue if they did not want a return value. I think this function should be able to assert mHasReturnValue up front.
I realize that makes it a slight bit more complicated for this intermediate step where we want to build EvaluateString on top of it (because now we need to make the ExtractReturnValue call conditional). But we're about to remove EvaluateString anyway, right?
>+++ b/dom/base/nsJSUtils.h
>+ // Compartment in which we should switch to for the execution of the script.
Maybe:
// Handles switching to our global's compartment.
?
>+ bool mHasReturnValue;
Maybe mWantReturnValue?
>+ bool mExpectScopeChain;
This seems to be unused. Should we be asserting something about it somewhere? Perhaps in SyncAndExec asserting that it's false?
>+ // must come from an AutoJSAPI that has had TakeOwnershipOfErrorReporting()
Actually, it needs to come from an AutoEntryScript (a slightly more stringent requirement). Yes, I know the comment used to say what this says; it never got updated.
>+ // option should be set to false. The returned value would be wrapped in the
>+ // |aCx| compartment when |exitCompartment| is called.
There is no exitCompartment....
I'm not actually sure we need a SetReturnValue option at all. It's only valid to use with CompileAndExec, right? And CompileAndExec already takes a CompileOptions. So can we not just examine .noScriptRval in CompileAndExec to set mWantsReturnValue?
That would not allow us to eagerly assert in SetCoerceToString, but we could assert in CompileAndExec that !mCoerceToString || mWantsReturnValue.
>+ // Set the scope chain in which the execution of the code would be in.
"Set the scope chain in which the code should be executed."
>+ ExtractReturnValue(JS::MutableHandle<JS::Value> aRetValue);
This should document that the value will be returned in the compartment of the aGlobal passed to the constructor.
>+ MOZ_MUST_USE nsresult SyncAndExec(void **aOffThreadToken,
Document that the resulting script is returned in aScript?
r=me with the above nits. Thank you again for being so patient!
Attachment #8834889 -
Flags: review?(bzbarsky) → review+
Comment 18•9 years ago
|
||
> So can we not just examine .noScriptRval in CompileAndExec to set mWantsReturnValue?
Oh, I see. Some overloads of EvaluateString call setNoScriptRval right now. So I still think we don't need a SetReturnValue, but we do need to make sure that callers that do NOT want an rval call setNoScriptRval(), ok.
Comment 19•9 years ago
|
||
Comment on attachment 8834892 [details] [diff] [review]
part 2 - Replace nsJSUtils::EvaluateString calls by ExecuteContext scope.
>+++ b/dom/base/nsGlobalWindow.cpp
> JS::CompileOptions options(aes.cx());
This callsite needs to call setNoScriptRval(true), I believe.
>+++ b/dom/base/nsScriptLoader.cpp
The CompileOptions and FillCompileOptionsForRequest bits can move into the !aRequest->mOffThreadToken branch, right?
>+++ b/dom/jsurl/nsJSProtocolHandler.cpp
>+ if (NS_FAILED(rv)) {
>+ return rv;
>+ }
No, that's not right. We want to return NS_ERROR_MALFORMED_URI if rv is a failure code here.
This is why we set things up so ExtractReturnValue would do the right thing if a previous *Exec failed. The code here should look like this:
exec.CompileAndExec(options, NS_ConvertUTF8toUTF16(script));
rv = exec.ExtractReturnValue(&v);
imo.
> + if (!JS_WrapValue(cx, &v)) {
No need; please take this out. "v" is in the compartment of "globalJSObject", which is innerGlobal->GetGlobalJSObject(). cx comes from AutoEntryScript aes(innerGlobal, ...), which enters the compartment of innerGlobal->GetGlobalJSObject(). So v is already in the right compartment.
r=me with the above fixed.
Attachment #8834892 -
Flags: review?(bzbarsky) → review+
| Assignee | ||
Comment 20•9 years ago
|
||
(In reply to Boris Zbarsky [:bz] (still a bit busy) (if a patch has no decent message, automatic r-) from comment #19)
> Comment on attachment 8834892 [details] [diff] [review]
> part 2 - Replace nsJSUtils::EvaluateString calls by ExecuteContext scope.
>
> >+++ b/dom/base/nsGlobalWindow.cpp
>
> > JS::CompileOptions options(aes.cx());
>
> This callsite needs to call setNoScriptRval(true), I believe.
Good catch. I will toggle mWantsReturnValue in ExtractReturnValue and ensure that it is false in the ExecutionContext destructor to catch these issues, as mWantsReturnValue is set based on the CompileOptions during CompileAndExec function calls.
| Assignee | ||
Comment 21•9 years ago
|
||
(In reply to Boris Zbarsky [:bz] (still a bit busy) (if a patch has no decent message, automatic r-) from comment #19)
> Comment on attachment 8834892 [details] [diff] [review]
> part 2 - Replace nsJSUtils::EvaluateString calls by ExecuteContext scope.
>
> >+++ b/dom/base/nsScriptLoader.cpp
>
> The CompileOptions and FillCompileOptionsForRequest bits can move into the
> !aRequest->mOffThreadToken branch, right?
>
Right, but I will keep it like that for the moment, as I am adding a new branch which would depend on it being lifted to avoid redundant code as part of Bug 900784.
> > + if (!JS_WrapValue(cx, &v)) {
>
> No need; please take this out. "v" is in the compartment of
> "globalJSObject", which is innerGlobal->GetGlobalJSObject(). cx comes from
> AutoEntryScript aes(innerGlobal, ...), which enters the compartment of
> innerGlobal->GetGlobalJSObject(). So v is already in the right compartment.
Would it make sense to add an AssertSameCompartment call instead of the JS_WrapValue then?
Flags: needinfo?(bzbarsky)
Comment 22•9 years ago
|
||
> Would it make sense to add an AssertSameCompartment call instead of the JS_WrapValue then?
Yes. Unfortunately, we don't have a great version of that for Value, so it will require manual checking for v.isObject().
Flags: needinfo?(bzbarsky)
| Assignee | ||
Comment 23•9 years ago
|
||
Comment 24•9 years ago
|
||
Pushed by npierron@mozilla.com:
https://hg.mozilla.org/integration/mozilla-inbound/rev/b6a142776fee
part 1 - Reimplement EvaluateString using the ExecutionContext class. r=bz
https://hg.mozilla.org/integration/mozilla-inbound/rev/4e48165f8c8e
part 2 - Replace nsJSUtils::EvaluateString calls by ExecutionContext scopes. r=bz
Comment 25•9 years ago
|
||
| bugherder | ||
https://hg.mozilla.org/mozilla-central/rev/b6a142776fee
https://hg.mozilla.org/mozilla-central/rev/4e48165f8c8e
Status: ASSIGNED → RESOLVED
Closed: 9 years ago
status-firefox55:
--- → fixed
Resolution: --- → FIXED
Target Milestone: --- → mozilla55
You need to log in
before you can comment on or make changes to this bug.
Description
•