Skip to content

fix crash in unsafe replacement of undefined#1443

Closed
alexlamsl wants to merge 1 commit into
mishoo:masterfrom
alexlamsl:unsafe-undefined
Closed

fix crash in unsafe replacement of undefined#1443
alexlamsl wants to merge 1 commit into
mishoo:masterfrom
alexlamsl:unsafe-undefined

Conversation

@alexlamsl

Copy link
Copy Markdown
Collaborator

There is a call to AST_SymbolRef.reference() within compress.js, which is the optimisation rule of mapping undefined to variable of the same name, guarded under unsafe flag. As it does not pass in any mangle options object, this results in TypeError: Unable to get property 'keep_fnames' of undefined or null reference.

AST_SymbolRef.reference() is used to populate AST_Scope.enclosed array (also AST_SymbolRef.frame integer, but seems unused), which is only used by mangler.

Now in both command-line and API use cases, call to mangle_names() is preceded by a call to figure_out_scope(<mangle options>), so there is no need for code within compress.js to call AST_SymbolRef.reference() at all.

Comment thread test/compress/issue-1443.js Outdated
function n(n) {
return n * n;
}
return r === n;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How does it arrive at this result?

typeof n is "function"

"function" == "undefined" is false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What optimization does the triple equals come from?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is utilising the rule typeof a == "undefined" ➡️ a === undefined, specifically this line turns == into ===.

evaluate is not enabled in this test case, so typeof function(){} wouldn't get optimised any further.

@kzc kzc Jan 27, 2017

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In light of the fact that undefined is a pseudo variable that can be overridden in the parameter of f(undefined) is this expected result just accidentally correct because a local function will not triple equal anything passed into the outer function f? Had n in the original input code been an AST_Constant it could produce incorrect results.

I think this why that specific transform you mention above is in unsafe. It assumes that undefined is not overridden.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

(I'm little confused by your comment, so please bear with me...)

I added this test to ensure my removal of ref.refernce(); call within compress.js would not cause #1431 to resurface in the form of this unsafe optimisation on undefined.

The reason why I use the roundabout way of typeof n == "undefined" instead of plain undefined is to make sure I end up with AST_Undefined instead of the local variable by that same name (which can be verified by running the code snippet against master and see the program crash).

Not sure if I've answered all/any of your questions? 😅

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The problem I'm describing is related to the test itself and the unsafe typeof transform you cited above. undefined is an overridden variable that is no longer equivalent to void 0. The test in this PR accidentally produces correct output because the local function n will not triple equal anything passed as an argument to f().

Here's a concrete example of why the unsafe typeof transform is incorrect when undefined is overridden:

$ cat u.js
function f(undefined) {
    return function() {
        var n = 1;
        return typeof n == "undefined";
    };
}
console.log( f(1)() );

expected result:

$ node u.js
false

incorrect code generated:

$ uglifyjs u.js -b -c unsafe
function f(undefined) {
    return function() {
        var n = 1;
        return undefined === n;
    };
}

console.log(f(1)());

incorrect actual result:

$ uglifyjs u.js -b -c unsafe | node
true

@alexlamsl

Copy link
Copy Markdown
Collaborator Author

@kzc would the following tests more preferable? The difference here is they don't rely on unsafe to generate AST_Undefined.

unsafe_undefined: {
    options = {
        if_return: true,
        unsafe: true
    }
    mangle = {}
    input: {
        function f(undefined) {
            return function() {
                if (a)
                    return b;
                if (c)
                    return d;
            }
        }
    }
    expect: {
        function f(n) {
            return function() {
                if (a)
                    return b;
                if (c)
                    return d;
                else
                    return n;
            }
        }
    }
}

keep_fnames: {
    options = {
        if_return: true,
        unsafe: true
    }
    mangle = {
        keep_fnames: true
    }
    input: {
        function f(undefined) {
            return function() {
                function n(a) {
                    return a*a;
                }
                if (a)
                    return b;
                if (c)
                    return d;
            }
        }
    }
    expect: {
        function f(r) {
            return function() {
                function n(n) {
                    return n*n;
                }
                if (a)
                    return b;
                if (c)
                    return d;
                else
                    return r;
            }
        }
    }
}

@kzc

kzc commented Jan 28, 2017

Copy link
Copy Markdown
Contributor

That would be preferable since the previous expected code is not valid as explained here: #1443 (comment)

The test you had just happened to illustrate why the typeof transform was deemed to be unsafe.

@alexlamsl

Copy link
Copy Markdown
Collaborator Author

@kzc tests updated and pushed.

These new tests should still be vulnerable to the same problem, i.e. f(1) would still return 1 instead of undefined

I use this particular transformation in angular.js, where they would deliberately pass in one less parameter to make undefined a local variable. So capturing that whenever uglify-js generates new AST_Undefined gives shorter code.

@kzc

kzc commented Jan 28, 2017

Copy link
Copy Markdown
Contributor

These new tests should still be vulnerable to the same problem, i.e. f(1) would still return 1 instead of undefined

Redefining undefined is fraught with problems. I've read that coffeescript does something similar in generated catch clauses.

If the generated code is known to be incorrect - even with the unsafe=true option - please add a descriptive comment in the test as per:

https://github.com/mishoo/UglifyJS2/blob/7f8d72d9d37396f2da05d5d824f74bd414c30119/test/compress/screw-ie8.js#L49-L50

An issue for another PR: Is there a way to fix this unsafe typeof transform when undefined is redefined?

@alexlamsl

Copy link
Copy Markdown
Collaborator Author

Will add comment after 🎥 🍿

@alexlamsl

Copy link
Copy Markdown
Collaborator Author

@kzc comment added

As for your suggestion for that new PR, is the goal being able to apply typeof a == "undefined" without also enabling the local variable rule, which are both guarded behind unsafe at the moment thus loss of granularity?

If so, one suggestion would be to "graduate" the typeof optimisation from unsafe. It already makes sure the expression under test is not an undeclared symbol - are there other corner cases which this transformation would break?

@alexlamsl

Copy link
Copy Markdown
Collaborator Author

If I'm reading the history correctly, typeof a == "undefined" optimisation is put behind unsafe in #105, and the reason being <DOM Node>.property may throw exceptions in IE<9.

So I propose swapping unsafe for screw_ie8, may be even compressor.option("screw_ie8") || !(self.right.expression instanceof AST_PropAccess)

Comment thread test/compress/issue-1443.js Outdated
@@ -0,0 +1,68 @@
// these tests assume local variable "undefined" has undefined as value

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could this be rephrased as:

// tests assume that variable `undefined` not redefined and has `void 0` as value

Also, please put an empty line after this comment.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done.

@kzc

kzc commented Jan 28, 2017

Copy link
Copy Markdown
Contributor

I was not aware that #105 was the motivation behind the "unsafe" flag use in the typeof === undefined optimization, but in light of the redefining undefined bug it should probably be put behind "unsafe" in any case.

In the wild undefined is often used as a catch parameter as well as in function arguments.

remove extraneous call to AST_SymbolRef.reference()
@alexlamsl

Copy link
Copy Markdown
Collaborator Author

@kzc mind if we carry on the discussion of typeof === undefined in #1446 so it's easier to keep track?

alexlamsl added a commit to alexlamsl/UglifyJS that referenced this pull request Feb 3, 2017
@alexlamsl alexlamsl closed this in 11676f9 Feb 23, 2017
@alexlamsl
alexlamsl deleted the unsafe-undefined branch February 24, 2017 00:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants