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

process: make source map getter resistant against prototype tampering #30228

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 51 additions & 12 deletions lib/internal/source_map/source_map_cache.js
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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) {
Expand All @@ -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),
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
40 changes: 40 additions & 0 deletions test/parallel/test-worker-terminate-source-map.js
Original file line number Diff line number Diff line change
@@ -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');