From a21c348d93fba14715ef8c1c597a26328cf1a947 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 15 May 2019 23:26:57 +0800 Subject: [PATCH] improve sandbox fidelity (#3415) --- lib/compress.js | 13 +++-- test/compress.js | 100 +++++++++++++++++++-------------- test/compress/collapse_vars.js | 19 +++++++ test/compress/issue-1704.js | 10 ++-- test/compress/sandbox.js | 54 ++++++++++++++++++ test/sandbox.js | 17 +----- test/ufuzz.js | 38 +++++++------ 7 files changed, 167 insertions(+), 84 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index f7730cd5..5d6567f7 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -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 diff --git a/test/compress.js b/test/compress.js index 1293be68..7fdedd12 100644 --- a/test/compress.js +++ b/test/compress.js @@ -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; } } diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 1862687a..17835a89 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -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 +} diff --git a/test/compress/issue-1704.js b/test/compress/issue-1704.js index 043038e5..cfa40fd6 100644 --- a/test/compress/issue-1704.js +++ b/test/compress/issue-1704.js @@ -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 } diff --git a/test/compress/sandbox.js b/test/compress/sandbox.js index 6c2be933..252c3e14 100644 --- a/test/compress/sandbox.js +++ b/test/compress/sandbox.js @@ -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" +} diff --git a/test/sandbox.js b/test/sandbox.js index 396a08f1..27976d8e 100644 --- a/test/sandbox.js +++ b/test/sandbox.js @@ -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); - } } }; diff --git a/test/ufuzz.js b/test/ufuzz.js index 4061e8d7..0cf1a0fe 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -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:"); -- 2.34.1