Skip to content
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

mangle bug with eval #3768

Closed
kzc opened this issue Apr 10, 2020 · 14 comments · Fixed by #3769
Closed

mangle bug with eval #3768

kzc opened this issue Apr 10, 2020 · 14 comments · Fixed by #3769

Comments

@kzc
Copy link
Contributor

kzc commented Apr 10, 2020

Given:

$ bin/uglifyjs -V
uglify-js 3.8.1
$ cat evalbug.js

        var e = eval, x = "PASS";
        (function() {
            console.log(e("x"));
        })();

Expected:

$ cat evalbug.js | node
PASS

Actual:

$ cat evalbug.js | bin/uglifyjs -m toplevel
var o=eval,l="PASS";(function(){console.log(o("x"))})();
$ cat evalbug.js | bin/uglifyjs -m toplevel | node
undefined:1
x
^
ReferenceError: x is not defined

The fix:

--- a/lib/scope.js
+++ b/lib/scope.js
@@ -165 +165 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options) {
-            if (name == "eval" && tw.parent() instanceof AST_Call) {
+            if (name == "eval") {

With fix:

$ cat evalbug.js | bin/uglifyjs -m toplevel
var e=eval,x="PASS";(function(){console.log(e("x"))})();
@kzc
Copy link
Contributor Author

kzc commented Apr 10, 2020

@alexlamsl alexlamsl added the bug label Apr 10, 2020
@alexlamsl
Copy link
Collaborator

Thanks for the report & fix − learnt something new about eval!

@alexlamsl
Copy link
Collaborator

alexlamsl commented Apr 10, 2020

Ah, so it was just a case of giving up due to the many peculiar behaviour of eval in the past...

$ cat test.js
console.log(function() {
    var a = 42;
    return eval("typeof a");
}(), function(e) {
    var a = null;
    return e("typeof a");
}(eval), function(eval) {
    var a = false;
    return eval("typeof a");
}(eval), function(f) {
    var a = "STRING";
    var eval = f;
    return eval("typeof a");
}(eval));

$ cat test.js | node
number undefined boolean string

That wiki link would be more helpful if it also specifies the holding variable name cannot be eval at the point of invocation, or that trick wouldn't work either 👻

@alexlamsl
Copy link
Collaborator

Since you've specified toplevel, the test script should be executed as node evalbug.js instead of cat evalbug.js | node

@alexlamsl alexlamsl removed the bug label Apr 10, 2020
alexlamsl added a commit to alexlamsl/UglifyJS that referenced this issue Apr 10, 2020
@alexlamsl
Copy link
Collaborator

It appears the current implementation is actually accurate, nonetheless let's add these test cases.

@kzc
Copy link
Contributor Author

kzc commented Apr 10, 2020

Although the Node results are not conclusive as you pointed out:

$ cat evalbug.js | node
PASS
$ node evalbug.js
undefined:1
x
^
ReferenceError: x is not defined

if evalbug is run in the browser it produces "PASS":

$ cat evalbug.html 
<!DOCTYPE html>
<html>
<body>
  <script>
    var e = eval, x = "PASS";
    (function() {
      alert(e("x"));
    })();
  </script>
</body>
</html>

I think the browser behavior is what we want to preserve.

@alexlamsl
Copy link
Collaborator

In that browser case, you are putting the code on the root level, so you can't mangle with toplevel: true, no? 🤔

@kzc
Copy link
Contributor Author

kzc commented Apr 10, 2020

I don't see why one could not mangle with toplevel true. All the symbol names within the purview of eval are preserved with the patch. The patch should even be compatible with your new test cases once the expect:ed results are updated accordingly. I don't see a downside with the patch.

@alexlamsl
Copy link
Collaborator

There is a huge downside, namely it disables a lot of compress/mangle cases − I run into that with your patch and npm test.

In the browser case above, one can do:

<!DOCTYPE html>
<html>
<body>
  <script>
    var x = "PASS";
    (function() {
      alert(x);
    })();
  </script>
  <h1>Hello World!</h1>
  <script>
    alert(x);
  </script>
</body>
</html>

No eval involved, but you still can't just go and rename that x in the first <script> section, which mangle:{toplevel:true} would have done.

@kzc
Copy link
Contributor Author

kzc commented Apr 10, 2020

The example above should still work with a name cache with toplevel mangle if the two script sections were independently minified. For that matter, x could not have been mangled in the second script segment because it's an unknown global.

toplevel and eval are independent concepts. There would be no optimization opportunities missed because the symbols involved with eval ought not be altered by the toplevel mangle in the first place. Mangling and optimization not within the reach of eval can still proceed.

I'm arguing for logical consistency between eval() and eval with toplevel mangle. Why is it that this case behaves differently with an unpatched uglify-js 3.8.1?

$ cat evalbug2.js

        eval(); // nop to change uglify-js 3.8.1 behavior
        var e = eval, x = "PASS";
        (function() {
            var VARIABLE_TO_BE_MANGLED = +Object(123);
            console.log(e("x"));
            console.log(VARIABLE_TO_BE_MANGLED, VARIABLE_TO_BE_MANGLED);
        })();
$ cat evalbug2.js | node
PASS
123 123
$ cat evalbug2.js | bin/uglifyjs -bm toplevel
eval();

var e = eval, x = "PASS";

(function() {
    var o = +Object(123);
    console.log(e("x"));
    console.log(o, o);
})();
$ cat evalbug2.js | bin/uglifyjs -bm toplevel | node
PASS
123 123

Think of the patch as a safeguard when used with toplevel to produce the correct result in the browser in more cases with eval.

@alexlamsl
Copy link
Collaborator

Why is it that this case behaves differently with an unpatched uglify-js 3.8.1?

As shown in #3768 (comment), the scope which eval will access depends on:

  • where it is invoked
  • whether it is called eval when it is invoked

In your second example you have invoked eval() at AST_Toplevel, so it will prohibits mangling there.

I still fail to see any inconsistencies − yes eval has magical behaviours, but I have yet to see a case which uglify-js fails to follow the specification.

(Will have a look at the nameCache case you've mentioned now...)

@alexlamsl
Copy link
Collaborator

Look at it this way − when you specified --mangle toplevel in your first exmaple, it's equivalent to running --mangle on:

(function() {
        var e = eval, x = "PASS";
        (function() {
            console.log(e("x"));
        })();
})();

Now, that e("x") call of eval will only have access to the AST_Toplevel, which is why e and x are free to be mangled.

@alexlamsl
Copy link
Collaborator

How about this − if we see eval but not invoked, we will just unconditionally mark AST_Toplevel.uses_eval?

Would that address your concern about separation of eval & toplevel? It certainly wouldn't pollute unnecessary child AST_Scope.uses_evals

@kzc
Copy link
Contributor Author

kzc commented Apr 10, 2020

but I have yet to see a case which uglify-js fails to follow the specification.

evalbug.html shows the issue quite clearly. If the code is mangled with toplevel with uglify-js it will not work in the browser. It ought to. None of the examples you have shown refute that, and the fix I proposed would be compatible with your new tests if expect: is updated accordingly. So no functionality is lost, and uglify-js will handle more cases correctly with toplevel mangle.

I guess it's pointless to debate this further. I'll use the fix in my fork.

@kzc kzc closed this as completed Apr 10, 2020
alexlamsl added a commit that referenced this issue Apr 10, 2020
alexlamsl added a commit to alexlamsl/UglifyJS that referenced this issue Apr 10, 2020
alexlamsl added a commit to alexlamsl/UglifyJS that referenced this issue Apr 10, 2020
alexlamsl added a commit to alexlamsl/UglifyJS that referenced this issue Apr 10, 2020
alexlamsl added a commit that referenced this issue Apr 10, 2020
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 a pull request may close this issue.

2 participants