Skip to content
Permalink
Browse files

process: make source map getter resistant against prototype tampering

Since this code runs during process and Worker shutdown, it should not
call user-provided code and thereby e.g. provide a way to break out of
`worker.terminate()`.

PR-URL: #30228
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Ben Coe <bencoe@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
  • Loading branch information
addaleax authored and MylesBorins committed Nov 2, 2019
1 parent 2e729f2 commit 56b3edcce814cda5cab92800262b7d66ab9d60f2
Showing with 91 additions and 12 deletions.
  1. +51 −12 lib/internal/source_map/source_map_cache.js
  2. +40 −0 test/parallel/test-worker-terminate-source-map.js
@@ -1,5 +1,28 @@
'use strict';

const {
JSON,
Object: {
create: ObjectCreate,
keys: ObjectKeys,
getOwnPropertyDescriptor: ObjectGetOwnPropertyDescriptor,
},
ObjectPrototype: {
hasOwnProperty: ObjectHasOwnProperty
},
MapPrototype: {
entries: MapEntries
}, uncurryThis
} = primordials;

const MapIteratorNext = uncurryThis(MapEntries(new Map()).next);
const WeakMapGet = uncurryThis(WeakMap.prototype.get);

function ObjectGetValueSafe(obj, key) {
const desc = ObjectGetOwnPropertyDescriptor(obj, key);
return ObjectHasOwnProperty(desc, 'value') ? desc.value : undefined;
}

// See https://sourcemaps.info/spec.html for SourceMap V3 specification.
const { Buffer } = require('buffer');
const debug = require('internal/util/debuglog').debuglog('source_map');
@@ -9,14 +32,14 @@ const { getOptionValue } = require('internal/options');
const {
normalizeReferrerURL,
} = require('internal/modules/cjs/helpers');
const { JSON, Object } = primordials;
// For cjs, since Module._cache is exposed to users, we use a WeakMap
// keyed on module, facilitating garbage collection.
const cjsSourceMapCache = new WeakMap();
// The esm cache is not exposed to users, so we can use a Map keyed
// on filenames.
const esmSourceMapCache = new Map();
const { fileURLToPath, URL } = require('url');
let Module;

let experimentalSourceMaps;
function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
@@ -40,6 +63,7 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
const data = dataFromUrl(basePath, match.groups.sourceMappingURL);
const url = data ? null : match.groups.sourceMappingURL;
if (cjsModuleInstance) {
if (!Module) Module = require('internal/modules/cjs/loader').Module;
cjsSourceMapCache.set(cjsModuleInstance, {
filename,
lineLengths: lineLengths(content),
@@ -148,17 +172,27 @@ function rekeySourceMap(cjsModuleInstance, newInstance) {
}
}

// WARNING: The `sourceMapCacheToObject` and `appendCJSCache` run during
// shutdown. In particular, they also run when Workers are terminated, making
// it important that they do not call out to any user-provided code, including
// built-in prototypes that might have been tampered with.

// Get serialized representation of source-map cache, this is used
// to persist a cache of source-maps to disk when NODE_V8_COVERAGE is enabled.
function sourceMapCacheToObject() {
const obj = Object.create(null);
const obj = ObjectCreate(null);

for (const [k, v] of esmSourceMapCache) {
const it = MapEntries(esmSourceMapCache);
let entry;
while (!(entry = MapIteratorNext(it)).done) {
const k = entry.value[0];
const v = entry.value[1];
obj[k] = v;
}

appendCJSCache(obj);

if (Object.keys(obj).length === 0) {
if (ObjectKeys(obj).length === 0) {
return undefined;
} else {
return obj;
@@ -171,23 +205,28 @@ function sourceMapCacheToObject() {
// TODO(bcoe): this means we don't currently serialize source-maps attached
// to error instances, only module instances.
function appendCJSCache(obj) {
const { Module } = require('internal/modules/cjs/loader');
Object.keys(Module._cache).forEach((key) => {
const value = cjsSourceMapCache.get(Module._cache[key]);
if (!Module) return;
const cjsModuleCache = ObjectGetValueSafe(Module, '_cache');
const cjsModules = ObjectKeys(cjsModuleCache);
for (let i = 0; i < cjsModules.length; i++) {
const key = cjsModules[i];
const module = ObjectGetValueSafe(cjsModuleCache, key);
const value = WeakMapGet(cjsSourceMapCache, module);
if (value) {
// This is okay because `obj` has a null prototype.
obj[`file://${key}`] = {
lineLengths: value.lineLengths,
data: value.data,
url: value.url
lineLengths: ObjectGetValueSafe(value, 'lineLengths'),
data: ObjectGetValueSafe(value, 'data'),
url: ObjectGetValueSafe(value, 'url')
};
}
});
}
}

// Attempt to lookup a source map, which is either attached to a file URI, or
// keyed on an error instance.
function findSourceMap(uri, error) {
const { Module } = require('internal/modules/cjs/loader');
if (!Module) Module = require('internal/modules/cjs/loader').Module;
let sourceMap = cjsSourceMapCache.get(Module._cache[uri]);
if (!uri.startsWith('file://')) uri = normalizeReferrerURL(uri);
if (sourceMap === undefined) {
@@ -0,0 +1,40 @@
'use strict';
const common = require('../common');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');

// Attempts to test that the source map JS code run on process shutdown
// does not call any user-defined JS code.

const { Worker, workerData, parentPort } = require('worker_threads');

if (!workerData) {
tmpdir.refresh();
process.env.NODE_V8_COVERAGE = tmpdir.path;

// Count the number of some calls that should not be made.
const callCount = new Int32Array(new SharedArrayBuffer(4));
const w = new Worker(__filename, { workerData: { callCount } });
w.on('message', common.mustCall(() => w.terminate()));
w.on('exit', common.mustCall(() => {
assert.strictEqual(callCount[0], 0);
}));
return;
}

const { callCount } = workerData;

function increaseCallCount() { callCount[0]++; }

// Increase the call count when a forbidden method is called.
Object.getPrototypeOf((new Map()).entries()).next = increaseCallCount;
Map.prototype.entries = increaseCallCount;
Object.keys = increaseCallCount;
Object.create = increaseCallCount;
Object.hasOwnProperty = increaseCallCount;
Object.defineProperty(Object.prototype, 'value', {
get: increaseCallCount,
set: increaseCallCount
});

parentPort.postMessage('done');

0 comments on commit 56b3edc

Please sign in to comment.
You can’t perform that action at this time.