Skip to content

Commit 6928103

Browse files
committed
feat: add vm2 sandbox support as vm runtime backup
1 parent 2bcec9e commit 6928103

File tree

8 files changed

+249
-49
lines changed

8 files changed

+249
-49
lines changed

pnpm-lock.yaml

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
+1-45
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { MonitorProvider } from './type.js';
2-
import ivm from 'isolated-vm';
3-
import { buildSandbox, environmentScript } from '../../../utils/sandbox.js';
4-
import { env } from '../../../utils/env.js';
2+
import { runCodeInVM } from '../../../utils/vm/index.js';
53

64
export const custom: MonitorProvider<{
75
code: string;
@@ -22,45 +20,3 @@ export const custom: MonitorProvider<{
2220
return result;
2321
},
2422
};
25-
26-
export async function runCodeInVM(_code: string) {
27-
const start = Date.now();
28-
const isolate = new ivm.Isolate({ memoryLimit: env.sandboxMemoryLimit });
29-
30-
// avoid end comment with line break
31-
const code = `${environmentScript}
32-
33-
;(async () => {
34-
${_code}
35-
})()`;
36-
37-
const [context, script] = await Promise.all([
38-
isolate.createContext(),
39-
isolate.compileScript(code),
40-
]);
41-
42-
const logger: any[][] = [];
43-
44-
buildSandbox(context, {
45-
console: {
46-
log: (...args: any[]) => {
47-
logger.push(['log', Date.now(), ...args]);
48-
},
49-
warn: (...args: any[]) => {
50-
logger.push(['warn', Date.now(), ...args]);
51-
},
52-
error: (...args: any[]) => {
53-
logger.push(['error', Date.now(), ...args]);
54-
},
55-
},
56-
});
57-
58-
const res = await script.run(context, {
59-
promise: true,
60-
});
61-
62-
context.release();
63-
script.release();
64-
65-
return { logger, result: res, usage: Date.now() - start };
66-
}

src/server/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"trpc-to-openapi": "^2.1.3",
7878
"uuid": "^9.0.1",
7979
"vite-express": "^0.13.0",
80+
"vm2": "^3.9.19",
8081
"winston": "^3.17.0",
8182
"yup": "^1.6.1",
8283
"zeromq": "^6.3.0",

src/server/trpc/routers/monitor.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@ import {
2626
MonitorModelSchema,
2727
MonitorStatusPageModelSchema,
2828
} from '../../prisma/zod/index.js';
29-
import { runCodeInVM } from '../../model/monitor/provider/custom.js';
3029
import { createAuditLog } from '../../model/auditLog.js';
3130
import {
3231
MonitorInfoWithNotificationIds,
3332
monitorPublicInfoSchema,
3433
} from '../../model/_schema/monitor.js';
3534
import { monitorPageManager } from '../../model/monitor/page/manager.js';
3635
import { token } from '../../model/notification/token/index.js';
36+
import { runCodeInVM } from '../../utils/vm/index.js';
3737

3838
export const monitorRouter = router({
3939
all: workspaceProcedure

src/server/utils/env.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,12 @@ export const env = {
6969
allowRegister: checkEnvTrusty(process.env.ALLOW_REGISTER),
7070
allowOpenapi: checkEnvTrusty(process.env.ALLOW_OPENAPI ?? 'true'),
7171
websiteId: process.env.WEBSITE_ID,
72-
sandboxMemoryLimit: process.env.SANDBOX_MEMORY_LIMIT
73-
? Number(process.env.SANDBOX_MEMORY_LIMIT)
74-
: 16, // unit: MB
72+
sandbox: {
73+
useVM2: checkEnvTrusty(process.env.USE_VM2),
74+
memoryLimit: process.env.SANDBOX_MEMORY_LIMIT
75+
? Number(process.env.SANDBOX_MEMORY_LIMIT)
76+
: 16, // unit: MB
77+
},
7578
puppeteerExecutablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
7679
dbDebug: checkEnvTrusty(process.env.DB_DEBUG),
7780
amapToken: process.env.AMAP_TOKEN,

src/server/utils/vm/index.ts

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import ivm from 'isolated-vm';
2+
import { buildSandbox, environmentScript } from './sandbox.js';
3+
import { env } from '../env.js';
4+
import { runCodeInVM2 } from './sandbox-vm2.js';
5+
6+
export async function runCodeInVM(_code: string): Promise<{
7+
logger: any[][];
8+
result: any;
9+
usage: number;
10+
}> {
11+
const code = `(async () => {${_code}})();`;
12+
13+
try {
14+
// Try to use VM2 first if enabled via environment variable
15+
if (env.sandbox.useVM2) {
16+
try {
17+
const ret = await runCodeInVM2(code);
18+
return ret;
19+
} catch (err) {
20+
console.error(
21+
'[Monitor] VM2 execution failed, falling back to isolated-vm:',
22+
err
23+
);
24+
throw err;
25+
}
26+
} else {
27+
// Use isolated-vm
28+
try {
29+
const ret = await runCodeInIVM(code);
30+
return ret;
31+
} catch (err) {
32+
console.error('[Monitor] isolated-vm execution failed:', err);
33+
throw err;
34+
}
35+
}
36+
} catch (err) {
37+
console.error('[Monitor] Code execution failed:', err);
38+
throw err;
39+
}
40+
}
41+
42+
async function runCodeInIVM(_code: string) {
43+
const start = Date.now();
44+
const isolate = new ivm.Isolate({ memoryLimit: env.sandbox.memoryLimit });
45+
46+
// avoid end comment with line break
47+
const code = `${environmentScript}
48+
49+
;(async () => {
50+
${_code}
51+
})()`;
52+
53+
const [context, script] = await Promise.all([
54+
isolate.createContext(),
55+
isolate.compileScript(code),
56+
]);
57+
58+
const logger: any[][] = [];
59+
60+
buildSandbox(context, {
61+
console: {
62+
log: (...args: any[]) => {
63+
logger.push(['log', Date.now(), ...args]);
64+
},
65+
warn: (...args: any[]) => {
66+
logger.push(['warn', Date.now(), ...args]);
67+
},
68+
error: (...args: any[]) => {
69+
logger.push(['error', Date.now(), ...args]);
70+
},
71+
},
72+
});
73+
74+
const res = await script.run(context, {
75+
promise: true,
76+
});
77+
78+
context.release();
79+
script.release();
80+
81+
return { logger, result: res, usage: Date.now() - start };
82+
}

src/server/utils/vm/sandbox-vm2.ts

+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { Worker } from 'worker_threads';
2+
import { env } from '../env.js';
3+
import path from 'path';
4+
import fs from 'fs';
5+
import { nanoid } from 'nanoid';
6+
import os from 'os';
7+
8+
// Function to create a temporary worker file
9+
async function createTempWorkerFile(
10+
code: string,
11+
memoryLimitMB: number
12+
): Promise<string> {
13+
const tempDir = os.tmpdir();
14+
const id = nanoid();
15+
const filePath = path.join(tempDir, `worker-${id}.js`);
16+
17+
// The worker script that will use VM2 to execute the code
18+
const workerScript = `
19+
const { parentPort } = require('worker_threads');
20+
const { VM } = require('vm2');
21+
22+
// Set up memory limit
23+
const memoryLimitBytes = ${memoryLimitMB} * 1024 * 1024;
24+
25+
// Create a sandbox with console logging
26+
const logger = [];
27+
28+
(async () => {
29+
try {
30+
const vm = new VM({
31+
timeout: 5000, // 5 seconds timeout
32+
sandbox: {
33+
console: {
34+
log: (...args) => {
35+
logger.push(['log', Date.now(), ...args]);
36+
},
37+
warn: (...args) => {
38+
logger.push(['warn', Date.now(), ...args]);
39+
},
40+
error: (...args) => {
41+
logger.push(['error', Date.now(), ...args]);
42+
}
43+
},
44+
request: async (config) => {
45+
// This is a simplified version of the request function
46+
// In a real implementation, you would use axios or another HTTP client
47+
const axios = require('axios');
48+
try {
49+
const result = await axios.request(config);
50+
return {
51+
headers: { ...result.headers },
52+
data: result.data,
53+
status: result.status
54+
};
55+
} catch (error) {
56+
throw new Error(error.message);
57+
}
58+
}
59+
},
60+
compiler: 'javascript',
61+
eval: false,
62+
wasm: false
63+
});
64+
65+
// Execute the code
66+
const start = Date.now();
67+
const result = await vm.run(${JSON.stringify(code)});
68+
const usage = Date.now() - start;
69+
70+
// Send the result back to the parent
71+
parentPort.postMessage({ success: true, result, logger, usage });
72+
} catch (error) {
73+
// Send the error back to the parent
74+
parentPort.postMessage({
75+
success: false,
76+
error: error.message,
77+
logger
78+
});
79+
}
80+
})();
81+
`;
82+
83+
await fs.promises.writeFile(filePath, workerScript);
84+
return filePath;
85+
}
86+
87+
// Function to run code in VM2 via worker threads
88+
export async function runCodeInVM2(
89+
code: string,
90+
memoryLimitMB = env.sandbox.memoryLimit
91+
): Promise<{ logger: any[][]; result: any; usage: number }> {
92+
const start = Date.now();
93+
94+
// Create a temporary worker file
95+
const workerFilePath = await createTempWorkerFile(code, memoryLimitMB);
96+
97+
return new Promise((resolve, reject) => {
98+
// Create a worker
99+
const worker = new Worker(workerFilePath);
100+
101+
// Set a timeout to kill the worker if it takes too long
102+
const timeout = setTimeout(() => {
103+
worker.terminate();
104+
fs.unlinkSync(workerFilePath);
105+
reject(new Error('Execution timed out'));
106+
}, 10000); // 10 seconds timeout
107+
108+
// Listen for messages from the worker
109+
worker.on('message', (message) => {
110+
clearTimeout(timeout);
111+
112+
// Clean up the temporary worker file
113+
fs.unlinkSync(workerFilePath);
114+
115+
if (message.success) {
116+
resolve({
117+
logger: message.logger || [],
118+
result: message.result,
119+
usage: message.usage || Date.now() - start,
120+
});
121+
} else {
122+
const error = new Error(message.error || 'Unknown error');
123+
// error.logger = message.logger || [];
124+
reject(error);
125+
}
126+
});
127+
128+
// Handle worker errors
129+
worker.on('error', (error) => {
130+
clearTimeout(timeout);
131+
fs.unlinkSync(workerFilePath);
132+
reject(error);
133+
});
134+
135+
// Handle worker exit
136+
worker.on('exit', (code) => {
137+
clearTimeout(timeout);
138+
if (code !== 0) {
139+
fs.unlinkSync(workerFilePath);
140+
reject(new Error(`Worker stopped with exit code ${code}`));
141+
}
142+
});
143+
});
144+
}
File renamed without changes.

0 commit comments

Comments
 (0)