improve sandbox fidelity (#3415)
authorAlex Lam S.L <alexlamsl@gmail.com>
Wed, 15 May 2019 15:26:57 +0000 (23:26 +0800)
committerGitHub <noreply@github.com>
Wed, 15 May 2019 15:26:57 +0000 (23:26 +0800)
lib/compress.js
test/compress.js
test/compress/collapse_vars.js
test/compress/issue-1704.js
test/compress/sandbox.js
test/sandbox.js
test/ufuzz.js

index f7730cd..5d6567f 100644 (file)
@@ -451,6 +451,7 @@ merge(Compressor.prototype, {
             if (tw.safe_ids[def.id]) {
                 if (def.fixed == null) {
                     if (is_arguments(def)) return false;
+                    if (def.global && def.name == "arguments") return false;
                     def.fixed = make_node(AST_Undefined, def.orig);
                 }
                 return true;
@@ -894,9 +895,13 @@ merge(Compressor.prototype, {
         return orig.length == 1 && orig[0] instanceof AST_SymbolLambda;
     });
 
-    function is_lhs_read_only(lhs) {
+    function is_lhs_read_only(lhs, compressor) {
         if (lhs instanceof AST_This) return true;
-        if (lhs instanceof AST_SymbolRef) return lhs.definition().orig[0] instanceof AST_SymbolLambda;
+        if (lhs instanceof AST_SymbolRef) {
+            var def = lhs.definition();
+            return def.orig[0] instanceof AST_SymbolLambda
+                || compressor.exposed(def) && identifier_atom[def.name];
+        }
         if (lhs instanceof AST_PropAccess) {
             lhs = lhs.expression;
             if (lhs instanceof AST_SymbolRef) {
@@ -905,7 +910,7 @@ merge(Compressor.prototype, {
             }
             if (!lhs) return true;
             if (lhs.is_constant()) return true;
-            return is_lhs_read_only(lhs);
+            return is_lhs_read_only(lhs, compressor);
         }
         return false;
     }
@@ -1220,7 +1225,7 @@ merge(Compressor.prototype, {
                     var stop_if_hit = null;
                     var lhs = get_lhs(candidate);
                     var side_effects = lhs && lhs.has_side_effects(compressor);
-                    var scan_lhs = lhs && !side_effects && !is_lhs_read_only(lhs);
+                    var scan_lhs = lhs && !side_effects && !is_lhs_read_only(lhs, compressor);
                     var scan_rhs = foldable(get_rhs(candidate));
                     if (!scan_lhs && !scan_rhs) continue;
                     // Locate symbols which may execute code outside of scanning range
index 1293be6..7fdedd1 100644 (file)
@@ -1,35 +1,47 @@
-#! /usr/bin/env node
-
 var assert = require("assert");
+var child_process = require("child_process");
 var fs = require("fs");
 var path = require("path");
 var sandbox = require("./sandbox");
 var semver = require("semver");
 var U = require("./node");
 
-var failures = 0;
-var failed_files = Object.create(null);
-var minify_options = require("./ufuzz.json").map(JSON.stringify);
+var file = process.argv[2];
 var dir = path.resolve(path.dirname(module.filename), "compress");
-fs.readdirSync(dir).filter(function(name) {
-    return /\.js$/i.test(name);
-}).forEach(function(file) {
+if (file) {
+    var minify_options = require("./ufuzz.json").map(JSON.stringify);
     log("--- {file}", { file: file });
     var tests = parse_test(path.resolve(dir, file));
-    for (var i in tests) if (!test_case(tests[i])) {
-        failures++;
-        failed_files[file] = 1;
-    }
-});
-if (failures) {
-    console.error();
-    console.error("!!! Failed " + failures + " test case(s).");
-    console.error("!!! " + Object.keys(failed_files).join(", "));
-    process.exit(1);
+    process.exit(Object.keys(tests).filter(function(name) {
+        return !test_case(tests[name]);
+    }).length);
+} else {
+    var files = fs.readdirSync(dir).filter(function(name) {
+        return /\.js$/i.test(name);
+    });
+    var failures = 0;
+    var failed_files = Object.create(null);
+    (function next() {
+        var file = files.shift();
+        if (file) {
+            child_process.spawn(process.argv[0], [ process.argv[1], file ], {
+                stdio: [ "ignore", 1, 2 ]
+            }).on("exit", function(code) {
+                if (code) {
+                    failures += code;
+                    failed_files[file] = code;
+                }
+                next();
+            });
+        } else if (failures) {
+            console.error();
+            console.error("!!! Failed " + failures + " test case(s).");
+            console.error("!!! " + Object.keys(failed_files).join(", "));
+            process.exit(1);
+        }
+    })();
 }
 
-/* -----[ utils ]----- */
-
 function evaluate(code) {
     if (code instanceof U.AST_Node) code = make_code(code, { beautify: true });
     return new Function("return(" + code + ")")();
@@ -160,7 +172,7 @@ function parse_test(file) {
 
 // Try to reminify original input with standard options
 // to see if it matches expect_stdout.
-function reminify(orig_options, input_code, input_formatted, expect_stdout) {
+function reminify(orig_options, input_code, input_formatted, stdout) {
     for (var i = 0; i < minify_options.length; i++) {
         var options = JSON.parse(minify_options[i]);
         if (options.compress) [
@@ -191,11 +203,12 @@ function reminify(orig_options, input_code, input_formatted, expect_stdout) {
             });
             return false;
         } else {
-            var stdout = run_code(result.code);
-            if (typeof expect_stdout != "string" && typeof stdout != "string" && expect_stdout.name == stdout.name) {
-                stdout = expect_stdout;
+            var expected = stdout[options.toplevel ? 1 : 0];
+            var actual = run_code(result.code, options.toplevel);
+            if (typeof expected != "string" && typeof actual != "string" && expected.name == actual.name) {
+                actual = expected;
             }
-            if (!sandbox.same_stdout(expect_stdout, stdout)) {
+            if (!sandbox.same_stdout(expected, actual)) {
                 log([
                     "!!! failed running reminified input",
                     "---INPUT---",
@@ -214,10 +227,10 @@ function reminify(orig_options, input_code, input_formatted, expect_stdout) {
                     input: input_formatted,
                     options: options_formatted,
                     output: result.code,
-                    expected_type: typeof expect_stdout == "string" ? "STDOUT" : "ERROR",
-                    expected: expect_stdout,
-                    actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR",
-                    actual: stdout,
+                    expected_type: typeof expected == "string" ? "STDOUT" : "ERROR",
+                    expected: expected,
+                    actual_type: typeof actual == "string" ? "STDOUT" : "ERROR",
+                    actual: actual,
                 });
                 return false;
             }
@@ -226,8 +239,8 @@ function reminify(orig_options, input_code, input_formatted, expect_stdout) {
     return true;
 }
 
-function run_code(code) {
-    var result = sandbox.run_code(code, true);
+function run_code(code, toplevel) {
+    var result = sandbox.run_code(code, toplevel);
     return typeof result == "string" ? result.replace(/\u001b\[\d+m/g, "") : result;
 }
 
@@ -359,13 +372,14 @@ function test_case(test) {
             return false;
         }
     }
-    if (test.expect_stdout
-        && (!test.node_version || semver.satisfies(process.version, test.node_version))) {
-        var stdout = run_code(input_code);
+    if (test.expect_stdout && (!test.node_version || semver.satisfies(process.version, test.node_version))) {
+        var stdout = [ run_code(input_code), run_code(input_code, true) ];
+        var toplevel = test.options.toplevel;
+        var actual = stdout[toplevel ? 1 : 0];
         if (test.expect_stdout === true) {
-            test.expect_stdout = stdout;
+            test.expect_stdout = actual;
         }
-        if (!sandbox.same_stdout(test.expect_stdout, stdout)) {
+        if (!sandbox.same_stdout(test.expect_stdout, actual)) {
             log([
                 "!!! Invalid input or expected stdout",
                 "---INPUT---",
@@ -380,13 +394,13 @@ function test_case(test) {
                 input: input_formatted,
                 expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR",
                 expected: test.expect_stdout,
-                actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR",
-                actual: stdout,
+                actual_type: typeof actual == "string" ? "STDOUT" : "ERROR",
+                actual: actual,
             });
             return false;
         }
-        stdout = run_code(output);
-        if (!sandbox.same_stdout(test.expect_stdout, stdout)) {
+        actual = run_code(output, toplevel);
+        if (!sandbox.same_stdout(test.expect_stdout, actual)) {
             log([
                 "!!! failed",
                 "---INPUT---",
@@ -401,12 +415,12 @@ function test_case(test) {
                 input: input_formatted,
                 expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR",
                 expected: test.expect_stdout,
-                actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR",
-                actual: stdout,
+                actual_type: typeof actual == "string" ? "STDOUT" : "ERROR",
+                actual: actual,
             });
             return false;
         }
-        if (!reminify(test.options, input_code, input_formatted, test.expect_stdout)) {
+        if (!reminify(test.options, input_code, input_formatted, stdout)) {
             return false;
         }
     }
index 1862687..17835a8 100644 (file)
@@ -6178,3 +6178,22 @@ assign_undeclared: {
         "object",
     ]
 }
+
+Infinity_assignment: {
+    options = {
+        collapse_vars: true,
+        pure_getters: "strict",
+        unsafe: true,
+    }
+    input: {
+        var Infinity;
+        Infinity = 42;
+        console.log(Infinity);
+    }
+    expect: {
+        var Infinity;
+        Infinity = 42;
+        console.log(Infinity);
+    }
+    expect_stdout: true
+}
index 043038e..cfa40fd 100644 (file)
@@ -366,7 +366,7 @@ mangle_catch_redef_3: {
         console.log(o);
     }
     expect_exact: 'var o="PASS";try{throw 0}catch(o){(function(){function c(){o="FAIL"}c(),c()})()}console.log(o);'
-    expect_stdout: "PASS"
+    expect_stdout: true
 }
 
 mangle_catch_redef_3_toplevel: {
@@ -389,10 +389,10 @@ mangle_catch_redef_3_toplevel: {
         console.log(o);
     }
     expect_exact: 'var c="PASS";try{throw 0}catch(c){(function(){function o(){c="FAIL"}o(),o()})()}console.log(c);'
-    expect_stdout: "PASS"
+    expect_stdout: true
 }
 
-mangle_catch_redef_ie8_3: {
+mangle_catch_redef_3_ie8: {
     mangle = {
         ie8: true,
         toplevel: false,
@@ -412,7 +412,7 @@ mangle_catch_redef_ie8_3: {
         console.log(o);
     }
     expect_exact: 'var o="PASS";try{throw 0}catch(o){(function(){function c(){o="FAIL"}c(),c()})()}console.log(o);'
-    expect_stdout: "PASS"
+    expect_stdout: true
 }
 
 mangle_catch_redef_3_ie8_toplevel: {
@@ -435,5 +435,5 @@ mangle_catch_redef_3_ie8_toplevel: {
         console.log(o);
     }
     expect_exact: 'var c="PASS";try{throw 0}catch(c){(function(){function o(){c="FAIL"}o(),o()})()}console.log(c);'
-    expect_stdout: "PASS"
+    expect_stdout: true
 }
index 6c2be93..252c3e1 100644 (file)
@@ -12,3 +12,57 @@ console_log: {
         "% %s",
     ]
 }
+
+typeof_arguments: {
+    options = {
+        evaluate: true,
+        reduce_vars: true,
+        toplevel: true,
+        unused: true,
+    }
+    input: {
+        var arguments;
+        console.log((typeof arguments).length);
+    }
+    expect: {
+        var arguments;
+        console.log((typeof arguments).length);
+    }
+    expect_stdout: "6"
+}
+
+typeof_arguments_assigned: {
+    options = {
+        evaluate: true,
+        reduce_vars: true,
+        toplevel: true,
+        unused: true,
+    }
+    input: {
+        var arguments = void 0;
+        console.log((typeof arguments).length);
+    }
+    expect: {
+        console.log("undefined".length);
+    }
+    expect_stdout: "9"
+}
+
+toplevel_Infinity_NaN_undefined: {
+    options = {
+        evaluate: true,
+        reduce_vars: true,
+        toplevel: true,
+        unused: true,
+    }
+    input: {
+        var Infinity = "foo";
+        var NaN = 42;
+        var undefined = null;
+        console.log(Infinity, NaN, undefined);
+    }
+    expect: {
+        console.log("foo", 42, null);
+    }
+    expect_stdout: "foo 42 null"
+}
index 396a08f..27976d8 100644 (file)
@@ -52,32 +52,19 @@ function createContext() {
     return ctx;
 }
 
-var context;
-exports.run_code = function(code, reuse) {
+exports.run_code = function(code, toplevel) {
     var stdout = "";
     var original_write = process.stdout.write;
     process.stdout.write = function(chunk) {
         stdout += chunk;
     };
     try {
-        if (!reuse || !context) context = createContext();
-        vm.runInContext([
-            "!function() {",
-            code,
-            "}();",
-        ].join("\n"), context, { timeout: 5000 });
+        vm.runInContext(toplevel ? "(function(){" + code + "})()" : code, createContext(), { timeout: 5000 });
         return stdout;
     } catch (ex) {
         return ex;
     } finally {
         process.stdout.write = original_write;
-        if (!reuse || code.indexOf(".prototype") >= 0) {
-            context = null;
-        } else {
-            vm.runInContext(Object.keys(context).map(function(name) {
-                return "delete " + name;
-            }).join("\n"), context);
-        }
     }
 };
 
index 4061e8d..0cf1a0f 100644 (file)
@@ -969,7 +969,7 @@ function errorln(msg) {
     process.stderr.write("\n");
 }
 
-function try_beautify(code, result, printfn) {
+function try_beautify(code, toplevel, result, printfn) {
     var beautified = UglifyJS.minify(code, {
         compress: false,
         mangle: false,
@@ -981,7 +981,7 @@ function try_beautify(code, result, printfn) {
     if (beautified.error) {
         printfn("// !!! beautify failed !!!");
         printfn(beautified.error.stack);
-    } else if (sandbox.same_stdout(sandbox.run_code(beautified.code), result)) {
+    } else if (sandbox.same_stdout(sandbox.run_code(beautified.code, toplevel), result)) {
         printfn("// (beautified)");
         printfn(beautified.code);
         return;
@@ -1009,7 +1009,7 @@ function log_suspects(minify_options, component) {
                 errorln("Error testing options." + component + "." + name);
                 errorln(result.error.stack);
             } else {
-                var r = sandbox.run_code(result.code);
+                var r = sandbox.run_code(result.code, m.toplevel);
                 return sandbox.same_stdout(original_result, r);
             }
         }
@@ -1031,7 +1031,7 @@ function log_rename(options) {
         errorln("Error testing options.rename");
         errorln(result.error.stack);
     } else {
-        var r = sandbox.run_code(result.code);
+        var r = sandbox.run_code(result.code, m.toplevel);
         if (sandbox.same_stdout(original_result, r)) {
             errorln("Suspicious options:");
             errorln("  rename");
@@ -1045,23 +1045,24 @@ function log(options) {
     errorln("//=============================================================");
     if (!ok) errorln("// !!!!!! Failed... round " + round);
     errorln("// original code");
-    try_beautify(original_code, original_result, errorln);
+    try_beautify(original_code, false, original_result, errorln);
     errorln();
     errorln();
     errorln("//-------------------------------------------------------------");
+    options = JSON.parse(options);
     if (typeof uglify_code == "string") {
         errorln("// uglified code");
-        try_beautify(uglify_code, uglify_result, errorln);
+        try_beautify(uglify_code, options.toplevel, uglify_result, errorln);
         errorln();
         errorln();
         errorln("original result:");
-        errorln(typeof original_result == "string" ? original_result : original_result.stack);
+        errorln(errored ? original_result.stack : original_result);
         errorln("uglified result:");
         errorln(typeof uglify_result == "string" ? uglify_result : uglify_result.stack);
     } else {
         errorln("// !!! uglify failed !!!");
         errorln(uglify_code.stack);
-        if (typeof original_result != "string") {
+        if (errored) {
             errorln();
             errorln();
             errorln("original stacktrace:");
@@ -1069,7 +1070,6 @@ function log(options) {
         }
     }
     errorln("minify(options):");
-    options = JSON.parse(options);
     errorln(JSON.stringify(options, null, 2));
     errorln();
     if (!ok && typeof uglify_code == "string") {
@@ -1084,30 +1084,34 @@ var fallback_options = [ JSON.stringify({
     mangle: false
 }) ];
 var minify_options = require("./ufuzz.json").map(JSON.stringify);
-var original_code, original_result;
+var original_code, original_result, errored;
 var uglify_code, uglify_result, ok;
 for (var round = 1; round <= num_iterations; round++) {
     process.stdout.write(round + " of " + num_iterations + "\r");
 
     original_code = createTopLevelCode();
-    original_result = sandbox.run_code(original_code);
-    (typeof original_result != "string" ? fallback_options : minify_options).forEach(function(options) {
-        uglify_code = UglifyJS.minify(original_code, JSON.parse(options));
+    var orig_result = [ sandbox.run_code(original_code) ];
+    errored = typeof orig_result[0] != "string";
+    if (!errored) orig_result.push(sandbox.run_code(original_code, true));
+    (errored ? fallback_options : minify_options).forEach(function(options) {
+        var o = JSON.parse(options);
+        uglify_code = UglifyJS.minify(original_code, o);
+        original_result = orig_result[o.toplevel ? 1 : 0];
         if (!uglify_code.error) {
             uglify_code = uglify_code.code;
-            uglify_result = sandbox.run_code(uglify_code);
+            uglify_result = sandbox.run_code(uglify_code, o.toplevel);
             ok = sandbox.same_stdout(original_result, uglify_result);
         } else {
             uglify_code = uglify_code.error;
-            if (typeof original_result != "string") {
+            if (errored) {
                 ok = uglify_code.name == original_result.name;
             }
         }
         if (verbose || (verbose_interval && !(round % INTERVAL_COUNT)) || !ok) log(options);
-        else if (typeof original_result != "string") {
+        else if (errored) {
             println("//=============================================================");
             println("// original code");
-            try_beautify(original_code, original_result, println);
+            try_beautify(original_code, o.toplevel, original_result, println);
             println();
             println();
             println("original result:");