Skip to content
This repository has been archived by the owner on Feb 12, 2022. It is now read-only.

Fixed multiple ordering bugs involving unsafe a-temporal abstract values #2413

Closed
wants to merge 2 commits into from

Conversation

sb98052
Copy link

@sb98052 sb98052 commented Aug 10, 2018

Release notes: Fixed multiple ordering bugs

Fixes #2327 and #2406. The discussion around this set of issues can be found in #2327. To summarize, sometimes the serializer's CSE algorithm would relocate simplified values out of the path along which they were simplified. This relocation is unsafe for values that can cause side effects and ones that can throw. Otherwise, it may be wasteful but safe.

This PR helps improves the situation by making all potentially unsafe abstract values temporal. The cases I found came out of a code audit. Calls to createFromTemplate should have been covered comprehensively. Most unsafe operations still don't accept abstract values and throw introspection errors, so they are not a problem yet.

@sebmarkbage
Copy link
Contributor

sebmarkbage commented Aug 10, 2018

I don’t understand the criteria decided upon. How are not everything temporal according to this definition?

I believe that almost every expression in JavaScript can throw if given invalid input. So when should I not mark something as temporal?

(If we count TDZ, I believe literally everything can throw.)

@sb98052 sb98052 force-pushed the safe-templates branch 2 times, most recently from 740fdaf to ad27aca Compare August 10, 2018 17:01
@sb98052
Copy link
Author

sb98052 commented Aug 10, 2018

I think a more complete statement of the criterion would be "the value produced by the template can throw under a condition that is not precluded by the assumptions under which the template is used." This is the root of the specific bug that this PR tries to knock off, since such dynamic conditions are sensitive to simplifications by the simplifier.

The general problem of formalizing the constraints under which something can be atemporal and balancing those constraints with efficiency is still open, and this PR doesn't help with that.

Copy link
Contributor

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"the value produced by the template can throw under a condition that is not precluded by the assumptions under which the template is used"

This seems like circular reasoning to me. This can only be used under the conditions we use it.

I think the most useful definition is that something should be temporal if it can throw at runtime, read mutable state or have side-effects, even when the args to the abstract value have the types that are assumed/inferred by the interpreter.

The issue here is that we end up passing an incorrect type arg. That means that any expression has to be able to accept any type or at least always undefined. This is not useful.

I don't think it's acceptable that everything becomes temporal which is the natural consequence of this.

My concrete concern is that figuring out whether something needs to be temporal or not is hard work. We have already converted things to temporal that shouldn't be. As a result, we now have to go through all of the temporal values and reevaluate if they really should be which is hard work. As we convert more of these, that list grows and it becomes even harder. We have to stop the leak.

I would be much more happy if we just turned on a flag in AbstractValue to turn everything temporal automatically if we just want to work around a bug temporarily. That way we can just turn the flag off when we've fixed the issue that doesn't let us distinguish.

@@ -344,7 +344,10 @@ export function OrdinaryGetPartial(
Receiver: Value
): Value {
if (Receiver instanceof AbstractValue && Receiver.getType() === StringValue && P === "length") {
return AbstractValue.createFromTemplate(realm, lengthTemplate, NumberValue, [Receiver], lengthTemplateSrc);
return AbstractValue.createTemporalFromTemplate(realm, lengthTemplate, NumberValue, [Receiver], {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a simple example. Given that the type of the Receiver is a string, this can never throw, have side-effects nor read mutable state (since it's immutable). It can only error if this is fed with the wrong type of input arg. That needs to be fixed.

However, once we fix that, how will I know that this can be turned atemporal again? Not everything has this property. So I have to go through them all one-by-one and think hard if this can really throw or not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simple: if Receiver is temporal, the get call must be temporal. Otherwise it is a-temporal.

@sebmarkbage
Copy link
Contributor

sebmarkbage commented Aug 10, 2018

Another approach could be to require all abstract values to be derived using the generator but add a flag like isTemporal to distinguish things that could be reordered vs things that can't. As long as that information is preserved in the source code.

@sb98052
Copy link
Author

sb98052 commented Aug 10, 2018

"the value produced by the template can throw under a condition that is not precluded by the assumptions under which the template is used"

This seems like circular reasoning to me. This can only be used under the conditions we use it.

More like "This can only throw under the conditions in which it is expected to throw" which is not circular. It's the throwing under certain conditions that is precluded, not the conditions themselves. But my phrasing didn't help in making this clear.

That said, the distinction is irrelevant if the precondition for a value to be atemporal is that it never throw at runtime.

I do see your concern - my approach makes some values atemporal in a conditional setting, but at the cost of making many others temporal, even in an unconditional setting. As a side effect of my change, calls to String.length and such would never get lifted, and that's bad.

I'll try to come up with an approach that strikes a better balance, given these parameters.

Copy link
Contributor

@hermanventer hermanventer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be a work in progress and not ready for merging just yet.

// "in" and "instanceof" can throw and therefore must result in temporals
if (result instanceof AbstractValue && (op === "in" || op === "instanceof")) {
invariant(realm.generator !== undefined);
result = realm.generator.deriveAbstract(result.types, result.values, result.args, result.operationDescriptor);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only AbstractValue methods should ever call deriveAbstract directly.

return AbstractValue.createFromBinaryOp(realm, op, lval, rval, loc);
let result = AbstractValue.createFromBinaryOp(realm, op, lval, rval, loc);

// "in" and "instanceof" can throw and therefore must result in temporals
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If getPureBinaryOperationResultType does not throw, it should not be the case that the binary operation may throw or have a side effect.

@@ -317,6 +317,8 @@ const JSONParse = buildExpressionTemplate(JSONParseStr);
function InternalJSONClone(realm: Realm, val: Value): Value {
if (val instanceof AbstractValue) {
if (val instanceof AbstractObjectValue) {
// These may need to use the temporal variants of createFromTemplate
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In particular, if val is temporal, then the derived values must be temporal as well. The reason being that val is known to be an object (and thus not null or undefined) at this temporal point, but may be null or undefined at a different temporal point. Since stringify and parse are not total when their arguments are null or undefined, they cannot become a-temporal when their arguments are not also a-temporal.

@@ -317,6 +317,8 @@ const JSONParse = buildExpressionTemplate(JSONParseStr);
function InternalJSONClone(realm: Realm, val: Value): Value {
if (val instanceof AbstractValue) {
if (val instanceof AbstractObjectValue) {
// These may need to use the temporal variants of createFromTemplate
// See #2327
let strVal = AbstractValue.createFromTemplate(realm, JSONStringify, StringValue, [val], JSONStringifyStr);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you fixing things or just adding a todo comment? If the former, this is not complete, if the latter then please add TODO to the comment along with an issue number.

@@ -577,7 +577,10 @@ export default function(realm: Realm, obj: ObjectValue): ObjectValue {
let O = RequireObjectCoercible(realm, context);

if (O instanceof AbstractValue && O.getType() === StringValue) {
return AbstractValue.createFromTemplate(realm, sliceTemplate, StringValue, [O, start, end], sliceTemplateSrc);
return AbstractValue.createTemporalFromTemplate(realm, sliceTemplate, StringValue, [O, start, end], {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You only need to to do this if O is temporal.

[O, separator, limit],
splitTemplateSrc
);
return AbstractValue.createTemporalFromTemplate(realm, splitTemplate, ObjectValue, [O, separator, limit], {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if O is temporal.

@@ -344,7 +344,10 @@ export function OrdinaryGetPartial(
Receiver: Value
): Value {
if (Receiver instanceof AbstractValue && Receiver.getType() === StringValue && P === "length") {
return AbstractValue.createFromTemplate(realm, lengthTemplate, NumberValue, [Receiver], lengthTemplateSrc);
return AbstractValue.createTemporalFromTemplate(realm, lengthTemplate, NumberValue, [Receiver], {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simple: if Receiver is temporal, the get call must be temporal. Otherwise it is a-temporal.

@sb98052
Copy link
Author

sb98052 commented Aug 13, 2018

Very nice. Herman's insight strikes the balance we want, and also refines the criterion. In general, a value must be temporal if it is dynamic (can throw, side effects). The assumption that something is not dynamic may itself rely on dynamic parameters - and those should be temporal. Conversely, if a parameter is temporal, then a value that relies on such an assumption becomes temporal too.

I've updated my implementation to do this. Also, I have added inverted tests to make sure that the operations I have touched get lifted in the unconditional case.

This is still work in progress because turning on the currently commented out invariant in createAbstractConcreteValue breaks a few things. Also, some tests are breaking and have to be fixed.

@sb98052 sb98052 added the WIP This pull request is a work in progress and not ready for merging. label Aug 13, 2018
@sebmarkbage
Copy link
Contributor

I'm still not satisfied with that definition. If an abstract value is atemporal but depends on a temporal value, and is depended upon by a temporal (everything is eventually depended upon by a generator entry). That only means that it has to be computed between those two temporal points. We can still move it relative to other temporal points. That information is already there in the graph and can be computed by the visitor.

If we go with the definition for abstract values needing to be temporal if their dependencies are temporal, then we need to start modeling the dependency graph in the generators instead so that different entries in the generator can be reordered depending on their dependency graph.

Copy link
Contributor

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should probably just be modeled directly in AbstractValue. If any arg is temporal, the whole thing should be temporal for any abstract value.

Doing this on a one-by-one basis seems error prone to me. In backends like LLVM, the things that throw are not always the same as when generating JS neither.

Being specific in the interpreter about guessing whether the serialized form can throw or not seems problematic.

// may rely on a dynamic condition. We express this condition as an implication,
// rval is temporal => this operation is temporal
// See #2327
if ((op === "in" || op === "instanceof") && (rvalIsAbstract && rval.isTemporal()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather put this condition into a helper function named something like "operationMustBeTemporal".

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've deferred this to the refactoring in #2489.

Copy link
Contributor

@hermanventer hermanventer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a logic inversion that needs fixing and testing. There is also a commented out invariant that needs to be uncommented or deleted.

createOperationDescriptor("BINARY_EXPRESSION", { op }),
{ isPure: true, skipInvariant: true }
);
else return AbstractValue.createFromBinaryOp(realm, op, lval, rval, loc);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still a little bit disturbed by this. If one of the arguments is temporal, then moving the operation to point where the current path conditions do not apply may be safe, but it seems hardly desirable.

In essence, any operation over temporal values should result in a temporal value, as a rule, except for a few specific cases that are well documented.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to enforce this protocol in a follow up PR that addresses #2489. Doing so will involve auditing uses of all of the abstract value factory methods.

let obVal;

// If val is temporal, then this operation is also temporal. See #2327.
if (val.isTemporal()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic seems the inverse of what you intend. This seems to call for an additional test case.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was inverted. On the other hand, it is not obvious to me that InternalJSONClone can create the particular conditions that this PR is trying to fix. So I have dropped this check for now, and will audit in a follow up request.


// Abstract values in an abstract concrete union are temporal, as they are predicated
// on the conditions that preclude the concrete values in the union.
// invariant(abstractValue.isTemporal());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this commented out?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I should have emphasized that the implementation is work in progress. I am yet to audit all of the paths that lead to this invariant.

@sb98052 sb98052 changed the title Fixed multiple ordering bugs involving unsafe a-temporal abstract values Make non-exempt operations temporal if args are temporal Aug 27, 2018
@sb98052 sb98052 changed the title Make non-exempt operations temporal if args are temporal Fixed multiple ordering bugs involving unsafe a-temporal abstract values Aug 27, 2018
@trueadm
Copy link
Contributor

trueadm commented Aug 31, 2018

Is this PR still active? Seems you addressed most of these issues in other PRs?

@sb98052 sb98052 closed this Sep 1, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
CLA Signed WIP This pull request is a work in progress and not ready for merging.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants