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

test(xsnap): size limits #2681

Merged
merged 8 commits into from
Mar 29, 2021
Merged
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
48 changes: 30 additions & 18 deletions packages/xsnap/src/xsnap.c
Original file line number Diff line number Diff line change
Expand Up @@ -204,19 +204,7 @@ int main(int argc, char* argv[])
int error = 0;
int interval = 0;
int freeze = 0;
xsCreation _creation = {
16 * 1024 * 1024, /* initialChunkSize */
16 * 1024 * 1024, /* incrementalChunkSize */
1 * 1024 * 1024, /* initialHeapCount */
1 * 1024 * 1024, /* incrementalHeapCount */
4096, /* stackCount */
32000, /* keyCount */
1993, /* nameModulo */
127, /* symbolModulo */
8192 * 1024, /* parserBufferSize */
1993, /* parserTableModulo */
};
xsCreation* creation = &_creation;
int parserBufferSize = 8192 * 1024;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe let's make that an unsigned int? Since atoi accepts -1234, it might be an extra safety belt.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you want it to underflow and wrap around? I don't understand why.


txSnapshot snapshot = {
SNAPSHOT_SIGNATURE,
Expand Down Expand Up @@ -279,6 +267,15 @@ int main(int argc, char* argv[])
return 1;
}
}
else if (!strcmp(argv[argi], "-s")) {
argi++;
if (argi < argc)
parserBufferSize = 1024 * atoi(argv[argi]);
else {
fxPrintUsage();
return 1;
}
}
else if (!strcmp(argv[argi], "-v")) {
printf("xsnap %s (XS %d.%d.%d)\n", XSNAP_VERSION, XS_MAJOR_VERSION, XS_MINOR_VERSION, XS_PATCH_VERSION);
return 0;
Expand All @@ -287,6 +284,20 @@ int main(int argc, char* argv[])
return 1;
}
}
xsCreation _creation = {
16 * 1024 * 1024, /* initialChunkSize */
16 * 1024 * 1024, /* incrementalChunkSize */
1 * 1024 * 1024, /* initialHeapCount */
1 * 1024 * 1024, /* incrementalHeapCount */
4096, /* stackCount */
32000, /* keyCount */
1993, /* nameModulo */
127, /* symbolModulo */
parserBufferSize, /* parserBufferSize */
1993, /* parserTableModulo */
};
xsCreation* creation = &_creation;

if (gxCrankMeteringLimit) {
if (interval == 0)
interval = 1;
Expand Down Expand Up @@ -850,11 +861,12 @@ void fxPatchBuiltIns(txMachine* machine)

void fxPrintUsage()
{
printf("xsnap [-h] [-f] [-i <interval>] [-l <limit>] [-m] [-r <snapshot>] [-s] [-v]\n");
printf("xsnap [-h] [-f] [-i <interval>] [-l <limit>] [-s <size>] [-m] [-r <snapshot>] [-s] [-v]\n");
printf("\t-f: freeze the XS machine\n");
printf("\t-h: print this help message\n");
printf("\t-i <interval>: metering interval (default to 1)\n");
printf("\t-l <limit>: metering limit (default to none)\n");
printf("\t-s <size>: parser buffer size, in kB (default to 8192)\n");
printf("\t-r <snapshot>: read snapshot to create the XS machine\n");
printf("\t-v: print XS version\n");
}
Expand Down Expand Up @@ -992,28 +1004,28 @@ void fxAbort(txMachine* the, int status)
#ifdef mxDebug
fxDebugger(the, (char *)__FILE__, __LINE__);
#endif
fxExitToHost(the);
c_exit(status);
break;
case XS_NOT_ENOUGH_MEMORY_EXIT:
xsLog("memory full\n");
#ifdef mxDebug
fxDebugger(the, (char *)__FILE__, __LINE__);
#endif
fxExitToHost(the);
c_exit(status);
break;
case XS_NO_MORE_KEYS_EXIT:
xsLog("not enough keys\n");
#ifdef mxDebug
fxDebugger(the, (char *)__FILE__, __LINE__);
#endif
fxExitToHost(the);
c_exit(status);
break;
case XS_TOO_MUCH_COMPUTATION_EXIT:
xsLog("too much computation\n");
#ifdef mxDebug
fxDebugger(the, (char *)__FILE__, __LINE__);
#endif
fxExitToHost(the);
c_exit(status);
break;
case XS_UNHANDLED_EXCEPTION_EXIT:
case XS_UNHANDLED_REJECTION_EXIT:
Expand Down
10 changes: 9 additions & 1 deletion packages/xsnap/src/xsnap.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ function echoCommand(arg) {
* @param {(request:Uint8Array) => Promise<Uint8Array>} [options.handleCommand]
* @param {string=} [options.name]
* @param {boolean=} [options.debug]
* @param {number=} [options.parserBufferSize] in kB (must be an integer)
* @param {string=} [options.snapshot]
* @param {'ignore' | 'inherit'} [options.stdout]
* @param {'ignore' | 'inherit'} [options.stderr]
Expand All @@ -58,6 +59,7 @@ export function xsnap(options) {
name = '<unnamed xsnap worker>',
handleCommand = echoCommand,
debug = false,
parserBufferSize = undefined,
snapshot = undefined,
stdout = 'ignore',
stderr = 'ignore',
Expand All @@ -82,10 +84,16 @@ export function xsnap(options) {
/** @type {Deferred<void>} */
const vatExit = defer();

const args = snapshot ? ['-r', snapshot] : [];
const args = [];
if (snapshot) {
args.push('-r', snapshot);
}
if (meteringLimit) {
args.push('-l', `${meteringLimit}`);
}
if (parserBufferSize) {
args.push('-s', `${parserBufferSize}`);
}

const xsnapProcess = spawn(bin, args, {
stdio: ['ignore', stdout, stderr, 'pipe', 'pipe'],
Expand Down
111 changes: 105 additions & 6 deletions packages/xsnap/test/test-xsnap.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* global setTimeout, __filename */
// eslint-disable-next-line import/no-extraneous-dependencies
import test from 'ava';
import * as childProcess from 'child_process';
import * as os from 'os';
Expand Down Expand Up @@ -48,18 +50,19 @@ test('evaluate until idle', async t => {
test('evaluate infinite loop', async t => {
const opts = options();
const vat = xsnap(opts);
t.teardown(vat.terminate);
await t.throwsAsync(vat.evaluate(`for (;;) {}`), {
message: 'xsnap test worker exited',
message: /xsnap test worker exited with code 7/,
instanceOf: Error,
});
await vat.close();
t.deepEqual([], opts.messages);
});

// TODO: Reenable when this doesn't take 3.6 seconds.
test.skip('evaluate promise loop', async t => {
test('evaluate promise loop', async t => {
const opts = options();
const vat = xsnap(opts);
t.teardown(vat.terminate);
await t.throwsAsync(
vat.evaluate(`
function f() {
Expand All @@ -68,11 +71,10 @@ test.skip('evaluate promise loop', async t => {
f();
`),
{
message: 'xsnap test worker exited',
message: /exited with code 7/,
instanceOf: Error,
},
);
await vat.close();
t.deepEqual([], opts.messages);
});

Expand Down Expand Up @@ -400,6 +402,103 @@ test('heap exhaustion: orderly fail-stop', async t => {
const vat = xsnap({ ...xsnapOptions, meteringLimit: 0, debug });
t.teardown(() => vat.terminate());
// eslint-disable-next-line no-await-in-loop
await t.throwsAsync(vat.evaluate(grow));
await t.throwsAsync(vat.evaluate(grow), { message: /exited with code 1$/ });
}
});

test('property name space exhaustion: orderly fail-stop', async t => {
const grow = qty => `
const objmap = {};
try {
for (let ix = 0; ix < ${qty}; ix += 1) {
const key = \`k\${ix}\`;
objmap[key] = 1;
if (!(key in objmap)) {
throw Error(key);
}
}
} catch (err) {
// name space exhaustion should not be catchable!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good, but if this managed to catch the first few times, and then stopped catching it, we'd not be able to distinguish that failure mode from the intended one. Would it be possible to have the catch clause raise an exception that would cause the worker to exit with something measurably different (something other than exited with code 6) ?

If that's too much hassle, just having it print a "wait a minute I shouldn't be printed" and manually eyeball it once should be sufficient.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand your point. The try ... catch is outside the for loop, so it catches either 0 or 1 times. If it catches, the vat.evaluate won't reject, so it'll fail t.throwsAsync.

In any case, I put a for-ever loop in the catch block to exit with "too much computation" in 0b4501f .

// spin and fail with "too much computation"
for (;;) {}
}
`;
for (const debug of [false, true]) {
const vat = xsnap({ ...xsnapOptions, meteringLimit: 0, debug });
t.teardown(() => vat.terminate());
console.log({ debug, qty: 31000 });
// eslint-disable-next-line no-await-in-loop
await t.notThrowsAsync(vat.evaluate(grow(31000)));
console.log({ debug, qty: 4000000000 });
// eslint-disable-next-line no-await-in-loop
await t.throwsAsync(vat.evaluate(grow(4000000000)), {
message: /exited with code 6/,
});
}
});

(() => {
const grow = qty => `
const send = it => issueCommand(ArrayBuffer.fromString(JSON.stringify(it)));
let expr = \`"\${Array(${qty}).fill('abcd').join('')}"\`;
try {
eval(expr);
send(expr.length);
} catch (err) {
send(err.message);
}
`;
for (const debug of [false, true]) {
for (const [parserBufferSize, qty, failure] of [
[undefined, 100, null],
[undefined, 8192 * 1024 + 100, 'buffer overflow'],
[2, 10, null],
[2, 50000, 'buffer overflow'],
]) {
test(`parser buffer size ${parserBufferSize ||
'default'}k; rep ${qty}; debug ${debug}`, async t => {
const opts = options();
const vat = xsnap({ ...opts, debug, parserBufferSize });
t.teardown(() => vat.terminate());
const expected = failure ? [failure] : [qty * 4 + 2];
// eslint-disable-next-line no-await-in-loop
await t.notThrowsAsync(vat.evaluate(grow(qty)));
t.deepEqual(
expected,
opts.messages.map(txt => JSON.parse(txt)),
);
});
}
}
})();

(() => {
const challenges = [
'new Uint8Array(2_130_706_417)',
'new Uint16Array(1_065_353_209)',
'new Uint32Array(532_676_605)',
'new BigUint64Array(266_338_303);',
'new Array(66_584_576).fill(0)',
'(new Array(66_584_575).fill(0))[66_584_575] = 0;',
];

for (const statement of challenges) {
test(`large sizes - abort cluster: ${statement}`, async t => {
const vat = xsnap(xsnapOptions);
t.teardown(() => vat.terminate());
// eslint-disable-next-line no-await-in-loop
await t.throwsAsync(
vat.evaluate(`
(() => {
try {
// can't catch memory full
${statement}\n
} catch (ignore) {
// ignore
}
})()`),
{ message: /exited with code 1$/ },
);
});
}
})();