Skip to content
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
36 changes: 34 additions & 2 deletions apps/sim/lib/execution/isolated-vm-worker.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ async function executeCode(request, executionId) {
const externalCopies = []

try {
isolate = new ivm.Isolate({ memoryLimit: 128 })
isolate = new ivm.Isolate({ memoryLimit: 256 })
if (executionId !== undefined) activeIsolates.set(executionId, isolate)
context = await isolate.createContext()
const jail = context.global
Expand Down Expand Up @@ -398,6 +398,21 @@ async function executeCode(request, executionId) {
}
}

if (
err.message.includes('Array buffer allocation failed') ||
err.message.includes('memory limit')
) {
return {
result: null,
stdout,
error: {
message:
'Execution exceeded memory limit (256 MB). Reduce image sizes or split the work into smaller batches.',
name: 'MemoryLimitError',
},
}
}
Comment thread
waleedlatif1 marked this conversation as resolved.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

OOM check is unreachable due to isDisposed ordering

High Severity

The new MemoryLimitError branches in both executeCode and executeTask are unreachable dead code. The isolate?.isDisposed check on lines 382 and 936 runs before the OOM message check. Since isolated-vm auto-disposes the isolate on OOM, isDisposed is always true when an out-of-memory error occurs, so the AbortError ("Execution cancelled") branch fires first. OOM errors will be misreported as user-initiated cancellations. The OOM check needs to be moved before the isDisposed guard to disambiguate the two cases.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b7db38a. Configure here.


return {
result: null,
stdout,
Expand Down Expand Up @@ -511,7 +526,7 @@ async function executeTask(request, executionId) {
let tPhase = tStart

try {
isolate = new ivm.Isolate({ memoryLimit: 128 })
isolate = new ivm.Isolate({ memoryLimit: 256 })
if (executionId !== undefined) activeIsolates.set(executionId, isolate)
context = await isolate.createContext()
const jail = context.global
Expand Down Expand Up @@ -937,6 +952,23 @@ async function executeTask(request, executionId) {
timings,
}
}

if (
err.message?.includes('Array buffer allocation failed') ||
err.message?.includes('memory limit')
) {
return {
result: null,
stdout,
error: {
message:
'Execution exceeded memory limit (256 MB). Reduce image sizes or split the work into smaller batches.',
name: 'MemoryLimitError',
},
timings,
}
}
Comment thread
waleedlatif1 marked this conversation as resolved.

return {
result: null,
stdout,
Expand Down
56 changes: 55 additions & 1 deletion apps/sim/sandbox-tasks/docx-generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,64 @@ export const docxGenerateTask = defineSandboxTask<SandboxTaskInput>({
globalThis.addSection = (section) => {
globalThis.__docxSections.push(section);
};
globalThis.getFileBase64 = async (fileId) => {

// Page geometry constants (twips, 1 twip = 1/1440 inch) for US Letter
globalThis.PAGE_W = 12240; // 8.5"
globalThis.PAGE_H = 15840; // 11"
globalThis.MARGIN = 1440; // 1" margins
globalThis.CONTENT_W = 9360; // PAGE_W - 2 * MARGIN

// 6 MB raw ≈ 8 MB base64; reject above this to avoid sandbox OOM.
const _MAX_IMG_B64 = 8 * 1024 * 1024;

/**
* getFileBase64(fileId) — load a workspace file as a full data URI string.
* Returns the complete "data:image/png;base64,..." string.
* Use addImage() rather than passing this directly to ImageRun.
*/
globalThis.getFileBase64 = async function getFileBase64(fileId) {
if (!fileId || typeof fileId !== 'string') {
throw new Error('getFileBase64: fileId must be a non-empty string');
}
const res = await globalThis.__brokers.workspaceFile({ fileId });
if (!res || !res.dataUri) {
throw new Error('getFileBase64: broker returned no data for file ' + fileId);
}
if (res.dataUri.length > _MAX_IMG_B64) {
throw new Error(
'getFileBase64: image exceeds the 6 MB embed limit (~8 MB base64). Use a smaller/compressed image.'
);
}
return res.dataUri;
};

/**
* addImage(fileId, opts) — fetch a workspace file and return a docx.ImageRun.
* Required opts: width, height (pixels or EMUs via transformation option).
* Example:
* new docx.Paragraph({ children: [await addImage('abc123', { width: 200, height: 100 })] })
*/
globalThis.addImage = async function addImage(fileId, opts) {
if (!opts || opts.width == null || opts.height == null) {
throw new Error('addImage: opts must include width and height (in pixels)');
}
const dataUri = await globalThis.getFileBase64(fileId);
const comma = dataUri.indexOf(',');
if (comma === -1) throw new Error('addImage: invalid data URI (no comma separator)');
const header = dataUri.slice(0, comma);
const base64 = dataUri.slice(comma + 1);
const mime = header.split(';')[0].replace('data:', '');
const extMap = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/gif': 'gif', 'image/bmp': 'bmp', 'image/svg+xml': 'svg' };
const ext = extMap[mime];
if (!ext) throw new Error('addImage: unsupported image type "' + mime + '". Use PNG, JPEG, GIF, BMP, or SVG.');
Comment thread
waleedlatif1 marked this conversation as resolved.
if (!globalThis.Buffer) throw new Error('addImage: Buffer polyfill missing — ensure docx bundle is loaded');
const { width, height, type: _t, data: _d, transformation: userTransform, ...passThrough } = opts;
return new globalThis.docx.ImageRun(Object.assign(passThrough, {
data: globalThis.Buffer.from(base64, 'base64'),
type: ext,
transformation: Object.assign({ width, height }, userTransform || {}),
}));
};
`,
// JSZip's browser build doesn't support nodebuffer output, so we go through
// base64 and decode back to bytes inside the isolate (avoids DataURL / Blob).
Expand Down
63 changes: 57 additions & 6 deletions apps/sim/sandbox-tasks/pdf-generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,71 @@ export const pdfGenerateTask = defineSandboxTask<SandboxTaskInput>({
if (!PDFLib) throw new Error('pdf-lib bundle not loaded');
globalThis.PDFLib = PDFLib;
globalThis.pdf = await PDFLib.PDFDocument.create();
globalThis.embedImage = async (dataUri) => {

// Convenience shortcuts — avoids verbose PDFLib.rgb() / PDFLib.StandardFonts.Helvetica
globalThis.rgb = PDFLib.rgb;
globalThis.StandardFonts = PDFLib.StandardFonts;

// Page-size constants in points (1pt = 1/72 inch)
globalThis.LETTER = [612, 792]; // 8.5" × 11"
globalThis.A4 = [595.28, 841.89]; // 210mm × 297mm

// 6 MB raw ≈ 8 MB base64; reject above this to avoid sandbox OOM.
const _MAX_IMG_B64 = 8 * 1024 * 1024;

/**
* embedImage(dataUri) — embed a data-URI image into the active PDF document.
* Dispatches to embedPng or embedJpg based on MIME type.
*/
globalThis.embedImage = async function embedImage(dataUri) {
if (!dataUri || typeof dataUri !== 'string') {
throw new Error('embedImage: dataUri must be a non-empty string');
}
const comma = dataUri.indexOf(',');
if (comma === -1) throw new Error('embedImage: invalid data URI (no comma separator)');
const header = dataUri.slice(0, comma);
const base64 = dataUri.slice(comma + 1);
const binary = globalThis.Buffer ? globalThis.Buffer.from(base64, 'base64') : null;
if (!binary) throw new Error('Buffer polyfill missing');
if (!globalThis.Buffer) throw new Error('embedImage: Buffer polyfill missing');
const binary = globalThis.Buffer.from(base64, 'base64');
const mime = header.split(';')[0].split(':')[1] || '';
if (mime.includes('png')) return globalThis.pdf.embedPng(binary);
return globalThis.pdf.embedJpg(binary);
// image/jpg is non-standard but tolerated; the canonical MIME is image/jpeg
if (mime === 'image/png') return globalThis.pdf.embedPng(binary);
if (mime === 'image/jpeg' || mime === 'image/jpg') return globalThis.pdf.embedJpg(binary);
throw new Error('embedImage: only PNG and JPEG are supported (got ' + (mime || 'unknown — check data URI header') + ')');
};
globalThis.getFileBase64 = async (fileId) => {

/**
* getFileBase64(fileId) — load a workspace file as a data URI string.
*/
globalThis.getFileBase64 = async function getFileBase64(fileId) {
if (!fileId || typeof fileId !== 'string') {
throw new Error('getFileBase64: fileId must be a non-empty string');
}
const res = await globalThis.__brokers.workspaceFile({ fileId });
if (!res || !res.dataUri) {
throw new Error('getFileBase64: broker returned no data for file ' + fileId);
}
if (res.dataUri.length > _MAX_IMG_B64) {
throw new Error(
'getFileBase64: image exceeds the 6 MB embed limit (~8 MB base64). Use a smaller/compressed image.'
);
}
return res.dataUri;
};

/**
* drawImage(page, fileId, opts) — fetch a workspace file and draw it on the given page.
* Required opts: x, y, width, height (points).
* Example: await drawImage(page, 'abc123', { x: 50, y: 700, width: 200, height: 100 });
*/
globalThis.drawImage = async function drawImage(page, fileId, opts) {
if (!opts || opts.x == null || opts.y == null || opts.width == null || opts.height == null) {
throw new Error('drawImage: opts must include x, y, width, and height (in points)');
}
const dataUri = await globalThis.getFileBase64(fileId);
const img = await globalThis.embedImage(dataUri);
page.drawImage(img, opts);
};
Comment thread
waleedlatif1 marked this conversation as resolved.
`,
finalize: `
const pdf = globalThis.pdf;
Expand Down
47 changes: 45 additions & 2 deletions apps/sim/sandbox-tasks/pptx-generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,52 @@ export const pptxGenerateTask = defineSandboxTask<SandboxTaskInput>({
const PptxGenJS = globalThis.__bundles['pptxgenjs'];
if (!PptxGenJS) throw new Error('pptxgenjs bundle not loaded');
globalThis.pptx = new PptxGenJS();
globalThis.getFileBase64 = async (fileId) => {
globalThis.pptx.layout = 'LAYOUT_16x9';

// Slide geometry for LAYOUT_16x9 (inches)
globalThis.SLIDE_W = 10;
globalThis.SLIDE_H = 5.625;
globalThis.MARGIN = 0.5;
globalThis.CONTENT_W = 9; // SLIDE_W - 2 * MARGIN
globalThis.CONTENT_H = 3.8; // usable body height below a standard title row

// ── Image helpers ──────────────────────────────────────────────────────────
// 6 MB raw ≈ 8 MB base64; reject above this to avoid sandbox OOM.
const _MAX_IMG_B64 = 8 * 1024 * 1024;

/**
* getFileBase64(fileId) — load a workspace file as a data URI string.
* PptxGenJS data format: "image/png;base64,<data>" (no "data:" prefix).
* Use as: slide.addImage({ data: await getFileBase64(fileId), x, y, w, h })
*/
globalThis.getFileBase64 = async function getFileBase64(fileId) {
if (!fileId || typeof fileId !== 'string') {
throw new Error('getFileBase64: fileId must be a non-empty string');
}
const res = await globalThis.__brokers.workspaceFile({ fileId });
return res.dataUri;
if (!res || !res.dataUri) {
throw new Error('getFileBase64: broker returned no data for file ' + fileId);
}
if (res.dataUri.length > _MAX_IMG_B64) {
throw new Error(
'getFileBase64: image exceeds the 6 MB embed limit (~8 MB base64). Use a smaller/compressed image.'
);
}
// PptxGenJS expects "image/png;base64,..." — strip the leading "data:" if present
return res.dataUri.replace(/^data:/, '');
};

/**
* addImage(slide, fileId, opts) — fetch a workspace file and embed it.
* Required opts: x, y, w, h (inches).
* Example: await addImage(slide, 'abc123', { x: 0.5, y: 1, w: 2, h: 1 });
*/
globalThis.addImage = async function addImage(slide, fileId, opts) {
if (!opts || opts.x == null || opts.y == null || opts.w == null || opts.h == null) {
throw new Error('addImage: opts must include x, y, w, and h (in inches)');
}
const data = await globalThis.getFileBase64(fileId);
slide.addImage(Object.assign({}, opts, { data }));
};
Comment thread
waleedlatif1 marked this conversation as resolved.
`,
finalize: `
Expand Down
Loading