Skip to content

Commit 905061c

Browse files
committed
fix(end-to-end): metering works for some malicious code
1 parent 5c9e1e7 commit 905061c

File tree

7 files changed

+117
-33
lines changed

7 files changed

+117
-33
lines changed

packages/transform-metering/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"esm": "^3.2.5",
1717
"rollup": "^1.16.6",
1818
"rollup-plugin-node-resolve": "^5.2.0",
19-
"ses": "^0.6.3",
19+
"ses": "^0.6.4",
2020
"tap-spec": "^5.0.0",
2121
"tape": "^4.9.2",
2222
"tape-promise": "^4.0.0"

packages/transform-metering/src/constants.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ export const DEFAULT_REGEXP_ID_PREFIX = '$h\u200d_re_';
1414
export const DEFAULT_COMBINED_METER = 1e6;
1515
export const DEFAULT_ALLOCATE_METER = true;
1616
export const DEFAULT_COMPUTE_METER = true;
17-
export const DEFAULT_STACK_METER = 32000;
17+
export const DEFAULT_STACK_METER = 8000;

packages/transform-metering/src/endow.js

+26-16
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import harden from '@agoric/harden';
2-
import RE2 from 're2';
3-
42
import * as c from './constants';
53

6-
const { create, defineProperties, entries, fromEntries,
7-
getOwnPropertyDescriptors, getPrototypeOf } = Object;
4+
// We'd like to import this, but RE2 is cjs
5+
const RE2 = require('re2');
6+
7+
const {
8+
create,
9+
defineProperties,
10+
entries,
11+
fromEntries,
12+
getOwnPropertyDescriptors,
13+
getPrototypeOf,
14+
} = Object;
815

916
export function makeMeteringEndowments(
1017
meter,
@@ -13,15 +20,15 @@ export function makeMeteringEndowments(
1320
overrideMeterId = c.DEFAULT_METER_ID,
1421
) {
1522
const wrapped = new WeakMap();
23+
wrapped.set(meter, meter);
1624
const meterId = overrideMeterId;
1725

1826
const wrapDescriptor = desc =>
19-
fromEntries(entries(desc).map(([k, v]) =>
20-
[k, wrap(v)]
21-
));
27+
fromEntries(entries(desc).map(([k, v]) => [k, wrap(v)]));
2228

29+
const shadowedRegexp = globalsToShadow.RegExp;
2330
function wrap(target) {
24-
if (target === globalsToShadow.RegExp) {
31+
if (shadowedRegexp !== undefined && target === shadowedRegexp) {
2532
// Replace the RegExp object with RE2.
2633
target = RE2;
2734
}
@@ -70,19 +77,21 @@ export function makeMeteringEndowments(
7077
wrapped.set(wrapper, wrapper);
7178

7279
// Assign the wrapped descriptors to the wrapper.
73-
const descs = fromEntries(entries(getOwnPropertyDescriptors(target))
74-
.map(([k, v]) => [k, wrapDescriptor(v)]));
80+
const descs = fromEntries(
81+
entries(getOwnPropertyDescriptors(target)).map(([k, v]) => [
82+
k,
83+
wrapDescriptor(v),
84+
]),
85+
);
7586
defineProperties(wrapper, descs);
7687
return wrapper;
7788
}
7889

7990
// Shadow the wrapped globals with the wrapped endowments.
8091
const shadowDescs = create(null);
81-
entries(getOwnPropertyDescriptors(globalsToShadow)).forEach(
82-
([p, desc]) => {
83-
shadowDescs[p] = wrapDescriptor(desc);
84-
},
85-
);
92+
entries(getOwnPropertyDescriptors(globalsToShadow)).forEach(([p, desc]) => {
93+
shadowDescs[p] = wrapDescriptor(desc);
94+
});
8695

8796
entries(getOwnPropertyDescriptors(endowments)).forEach(([p, desc]) => {
8897
// We wrap the endowment descriptors, too.
@@ -100,5 +109,6 @@ export function makeMeteringEndowments(
100109
};
101110

102111
// Package up these endowments as an object.
103-
return create(null, shadowDescs);
112+
const e = create(null, shadowDescs);
113+
return e;
104114
}

packages/transform-metering/src/meter.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ const bigIntWord = typeof BigInt !== 'undefined' && BigInt(1 << 32);
1111
const bigIntZero = bigIntWord && BigInt(0);
1212

1313
// Stop deducting when we reach a negative number.
14-
const makeCounter = balance => {
14+
const makeCounter = initBalance => {
15+
let balance = initBalance;
1516
const counter = increment => {
1617
if (balance > 0) {
1718
balance += increment;
1819
}
1920
return balance;
2021
};
21-
counter.reset = newBalance => (balance = newBalance);
22+
counter.reset = (newBalance = undefined) =>
23+
(balance = newBalance === undefined ? initBalance : newBalance);
2224
return counter;
2325
};
2426

@@ -174,7 +176,7 @@ export function makeMeterAndResetters(maxima = {}) {
174176
// Allocate meters need both stack and compute meters.
175177
const meterAllocate = makeAllocateMeter(maybeAbort, meter, allocateCounter);
176178

177-
const makeResetter = cnt => newBalance => {
179+
const makeResetter = cnt => (newBalance = undefined) => {
178180
maybeAbort.reset();
179181
if (cnt) {
180182
cnt.reset(newBalance);

packages/transform-metering/src/transform.js

+14-8
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,11 @@ export function makeMeteringTransformer(
7070
const visitor = {
7171
// Ensure meter identifiers are generated by us, or abort.
7272
Identifier(path) {
73-
if ((path.node.name === meterId || path.node.name.startsWith(regexpIdPrefix))
74-
&& !path.node[METER_GENERATED]) {
73+
if (
74+
(path.node.name === meterId ||
75+
path.node.name.startsWith(regexpIdPrefix)) &&
76+
!path.node[METER_GENERATED]
77+
) {
7578
throw path.buildCodeFrameError(
7679
`Identifier ${path.node.name} is reserved for metering code`,
7780
);
@@ -134,7 +137,13 @@ const ${reid}=RegExp(${JSON.stringify(pattern)},${JSON.stringify(flags)});`);
134137

135138
const meteringTransform = {
136139
rewrite(ss) {
137-
const { source, endowments } = ss;
140+
const { src: source, endowments } = ss;
141+
if (!endowments[meterId]) {
142+
return ss;
143+
}
144+
145+
// Meter how much source code they want to use.
146+
endowments[meterId][c.METER_COMPUTE](source.length);
138147

139148
// Do the actual transform.
140149
const ast = parser(source);
@@ -158,14 +167,11 @@ const ${reid}=RegExp(${JSON.stringify(pattern)},${JSON.stringify(flags)});`);
158167
? `(function(){${reSource}return ${maybeSource}})()`
159168
: `${reSource}${maybeSource}`;
160169

161-
// Meter how much source code they created.
162-
endowments[meterId][c.METER_COMPUTE](actualSource.length);
163-
170+
// console.log('metered source:', actualSource);
164171
return {
165172
...ss,
166173
ast,
167-
endowments,
168-
source: actualSource,
174+
src: actualSource,
169175
};
170176
},
171177
};

packages/transform-metering/test/test-transform.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,16 @@ test('meter transform', async t => {
1717
const rewrite = (source, testName) => {
1818
let cMeter;
1919
const ss = meteringTransform.rewrite({
20-
source,
20+
src: source,
2121
endowments: {
2222
[meterId]: {
23-
[c.METER_COMPUTE]: units => cMeter = units,
23+
[c.METER_COMPUTE]: units => (cMeter = units),
2424
},
2525
},
2626
sourceType: 'script',
2727
});
28-
t.equals(cMeter, ss.source.length, `compute meter updated ${testName}`);
29-
return ss.source;
28+
t.equals(cMeter, source.length, `compute meter updated ${testName}`);
29+
return ss.src;
3030
};
3131

3232
t.throws(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/* global globalThis */
2+
/* eslint-disable no-await-in-loop */
3+
import test from 'tape-promise/tape';
4+
import * as babelCore from '@babel/core';
5+
import SES from 'ses';
6+
7+
import {
8+
makeMeterAndResetters,
9+
makeMeteringEndowments,
10+
makeMeteringTransformer,
11+
} from '../src/index';
12+
13+
test('metering end-to-end', async t => {
14+
try {
15+
const [meter, reset] = makeMeterAndResetters();
16+
const { meterId, meteringTransform } = makeMeteringTransformer(babelCore);
17+
const endowments = makeMeteringEndowments(meter, globalThis, {}, meterId);
18+
const transforms = [meteringTransform];
19+
20+
const s = SES.makeSESRootRealm({ transforms });
21+
22+
const myEval = src => {
23+
Object.values(reset).forEach(r => r());
24+
return s.evaluate(src, endowments);
25+
};
26+
27+
const src1 = `123; 456;`;
28+
t.equals(myEval(src1), 456, 'trivial source succeeds');
29+
30+
const src2 = `\
31+
function f() {
32+
f();
33+
return 1;
34+
}
35+
f();
36+
`;
37+
t.throws(
38+
() => myEval(src2),
39+
/Stack meter exceeded/,
40+
'stack overflow fails',
41+
);
42+
43+
const src3 = `\
44+
while (true) {}
45+
`;
46+
t.throws(
47+
() => myEval(src3),
48+
/Compute meter exceeded/,
49+
'infinite loop fails',
50+
);
51+
52+
const src4 = `\
53+
/(x+x+)+y/.test('x'.repeat(10000));
54+
`;
55+
t.equals(myEval(src4), false, `catastrophic backtracking doesn't happen`);
56+
57+
const src5 = `\
58+
new Array(1e6).map(Object.create)
59+
`;
60+
t.throws(() => myEval(src5), RangeError, 'long map fails');
61+
} catch (e) {
62+
t.isNot(e, e, 'unexpected exception');
63+
} finally {
64+
t.end();
65+
}
66+
});

0 commit comments

Comments
 (0)