Skip to content

Commit

Permalink
JS -- Add a sandbox based on quickjs
Browse files Browse the repository at this point in the history
 * quickjs-eval.js has been generated using /~https://github.com/calixteman/pdf.js.quickjs/
 * lazy load of sandbox code
  • Loading branch information
calixteman committed Nov 10, 2020
1 parent 83658c9 commit 9bbd5fc
Show file tree
Hide file tree
Showing 9 changed files with 344 additions and 7 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ external/webL10n/
external/cmapscompress/
external/builder/fixtures/
external/builder/fixtures_esprima/
external/quickjs/quickjs-eval.js
src/shared/cffStandardStrings.js
src/shared/fonts_utils.js
test/tmp/
Expand Down
42 changes: 42 additions & 0 deletions external/quickjs/quickjs-eval.js

Large diffs are not rendered by default.

73 changes: 66 additions & 7 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ var stream = require("stream");
var exec = require("child_process").exec;
var spawn = require("child_process").spawn;
var spawnSync = require("child_process").spawnSync;
var stripComments = require("gulp-strip-comments");
var streamqueue = require("streamqueue");
var merge = require("merge-stream");
var zip = require("gulp-zip");
Expand Down Expand Up @@ -105,6 +106,7 @@ const DEFINES = Object.freeze({
COMPONENTS: false,
LIB: false,
IMAGE_DECODERS: false,
NO_SOURCE_MAP: false,
});

function transform(charEncoding, transformFunction) {
Expand Down Expand Up @@ -182,7 +184,8 @@ function createWebpackConfig(defines, output) {
var enableSourceMaps =
!bundleDefines.MOZCENTRAL &&
!bundleDefines.CHROME &&
!bundleDefines.TESTING;
!bundleDefines.TESTING &&
!bundleDefines.NO_SOURCE_MAP;
var skipBabel = bundleDefines.SKIP_BABEL;

// `core-js` (see /~https://github.com/zloirock/core-js/issues/514),
Expand Down Expand Up @@ -342,6 +345,41 @@ function createScriptingBundle(defines) {
.pipe(replaceJSRootName(scriptingAMDName, "pdfjsScripting"));
}

function createSandboxBundle(defines, code) {
var sandboxAMDName = "pdfjs-dist/build/pdf.sandbox";
var sandboxOutputName = "pdf.sandbox.js";
var sandboxFileConfig = createWebpackConfig(defines, {
filename: sandboxOutputName,
library: sandboxAMDName,
libraryTarget: "umd",
umdNamedDefine: true,
});

// The code is the one from the bundle pdf.scripting.js
// so in order to have it in a string (which will be eval-ed
// in the sandbox) we must escape some chars.
// This way we've all the code (initialization+sandbox) in
// the same bundle.
code = code.replace(/["\\\n\t]/g, match => {
if (match === "\n") {
return "\\n";
}
if (match === "\t") {
return "\\t";
}
return `\\${match}`;
});
return (
gulp
.src("./src/scripting_api/quickjs-sandbox.js")
.pipe(webpack2Stream(sandboxFileConfig))
.pipe(replaceWebpackRequire())
.pipe(replaceJSRootName(sandboxAMDName, "pdfjsSandbox"))
// put the code in a string to be eval-ed in the sandbox
.pipe(replace("/* INITIALIZATION_CODE */", `${code}`))
);
}

function createWorkerBundle(defines) {
var workerAMDName = "pdfjs-dist/build/pdf.worker";
var workerOutputName = "pdf.worker.js";
Expand Down Expand Up @@ -493,6 +531,20 @@ function makeRef(done, bot) {
});
}

gulp.task("sandbox", function (done) {
const defines = builder.merge(DEFINES, { GENERIC: true });
const scriptingDefines = builder.merge(defines, { NO_SOURCE_MAP: true });
return createScriptingBundle(scriptingDefines)
.pipe(stripComments())
.pipe(gulp.dest(GENERIC_DIR + "build"))
.on("data", file => {
const content = file.contents.toString();
createSandboxBundle(defines, content)
.pipe(gulp.dest(GENERIC_DIR + "build"))
.on("end", done);
});
});

gulp.task("default", function (done) {
console.log("Available tasks:");
var tasks = Object.keys(gulp.registry().tasks());
Expand Down Expand Up @@ -725,6 +777,7 @@ function buildGeneric(defines, dir) {

return merge([
createMainBundle(defines).pipe(gulp.dest(dir + "build")),
createScriptingBundle(defines).pipe(gulp.dest(dir + "build")),
createWorkerBundle(defines).pipe(gulp.dest(dir + "build")),
createWebBundle(defines).pipe(gulp.dest(dir + "web")),
gulp.src(COMMON_WEB_FILES, { base: "web/" }).pipe(gulp.dest(dir + "web")),
Expand Down Expand Up @@ -760,13 +813,19 @@ function buildGeneric(defines, dir) {
// HTML5 browsers, which implement modern ECMAScript features.
gulp.task(
"generic",
gulp.series("buildnumber", "default_preferences", "locale", function () {
console.log();
console.log("### Creating generic viewer");
var defines = builder.merge(DEFINES, { GENERIC: true });
gulp.series(
"buildnumber",
"default_preferences",
"locale",
"sandbox",
function () {
console.log();
console.log("### Creating generic viewer");
var defines = builder.merge(DEFINES, { GENERIC: true });

return buildGeneric(defines, GENERIC_DIR);
})
return buildGeneric(defines, GENERIC_DIR);
}
)
);

// Builds the generic production viewer that should be compatible with most
Expand Down
100 changes: 100 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"gulp-postcss": "^9.0.0",
"gulp-rename": "^2.0.0",
"gulp-replace": "^1.0.0",
"gulp-strip-comments": "^2.5.2",
"gulp-zip": "^5.0.2",
"jasmine": "^3.6.3",
"jsdoc": "^3.6.6",
Expand Down
35 changes: 35 additions & 0 deletions src/scripting_api/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ class App extends PDFObject {

// used in proxy.js to check that this the object with the backdoor
this._isApp = true;

this._setTimeout = setTimeout;
this._clearTimeout = clearTimeout;
this._setInterval = setInterval;
this._clearInterval = clearInterval;
}

// This function is called thanks to the proxy
Expand All @@ -56,6 +61,36 @@ class App extends PDFObject {
) {
this._send({ command: "alert", value: cMsg });
}

clearInterval(oInterval) {
return this._clearInterval(oInterval);
}

clearTimeOut(oTime) {
return this._clearTimeout(oTime);
}

setInterval(cExpr, nMilliseconds = 1000) {
if (typeof cExpr !== "string") {
throw new TypeError("First argument of app.setInterval must be a string");
}
if (typeof nMilliseconds !== "number") {
throw new TypeError(
"Second argument of app.setInterval must be a number"
);
}
return this._setInterval(cExpr, nMilliseconds);
}

setTimeOut(cExpr, nMilliseconds = 1000) {
if (typeof cExpr !== "string") {
throw new TypeError("First argument of app.setTimeOut must be a string");
}
if (typeof nMilliseconds !== "number") {
throw new TypeError("Second argument of app.setTimeOut must be a number");
}
return this._setTimeout(cExpr, nMilliseconds);
}
}

export { App };
79 changes: 79 additions & 0 deletions src/scripting_api/quickjs-sandbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/* Copyright 2020 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import ModuleLoader from "../../external/quickjs/quickjs-eval.js";

class Sandbox {
constructor(module) {
this._evalInSandbox = module.cwrap("evalInSandbox", null, ["string"]);
this._dispatchEventName = null;
this.module = module;
}

create(data) {
const sandboxData = JSON.stringify(data);
const extra = [
"send",
"setTimeout",
"clearTimeout",
"setInterval",
"clearInterval",
"crackURL",
];
const code = [
"exports = Object.create(null);",
"module = Object.create(null);",
`data = ${sandboxData};`,
// Next line is replaced by code from initialization.js
// when we create the bundle for the sandbox.x
"/* INITIALIZATION_CODE */",
`module.exports.initSandbox(data, {${extra.join(",")}}, this);`,
"delete exports;",
"delete module;",
"delete data;",
...extra.map(name => `delete ${name};`),
];
this._evalInSandbox(code.join("\n"));
this._dispatchEventName = data.dispatchEventName;
this.dumpMemoryUse();
}

dispatchEvent(event) {
if (this._dispatchEventName === null) {
throw new Error("Sandbox must have been initialized");
}
event = JSON.stringify(event);
this._evalInSandbox(`app['${this._dispatchEventName}'](${event});`);
}

dumpMemoryUse() {
this.module.ccall("dumpMemoryUse", null, []);
}

nukeSandbox() {
this._dispatchEventName = null;
this.module.ccall("nukeSandbox", null, []);
this.module = null;
this._evalInSandbox = null;
}
}

function QuickJSSandbox() {
return ModuleLoader().then(module => {
return new Sandbox(module);
});
}

export { QuickJSSandbox };
Loading

0 comments on commit 9bbd5fc

Please sign in to comment.