mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-21 02:52:06 +00:00
fix(ai): validate large base64 without anchored regexp loops
Gemini TTS replies over ~4 MB of base64 (~65 s of audio) failed with
"Maximum call stack size exceeded". Once a process holds enough compiled
code, V8 compiles new regexps unoptimized, and `^[...]+={0,2}$` then
pushes a backtrack entry per character until its stack overflows.
TTS now searches for a character outside the alphabet instead. The raw
base64 icon/reference-image check drops its own anchored regexp;
decodeStrictBase64 already rejects the same inputs.
This commit is contained in:
1 parent
517efda38a
commit
80d36cd251
4 files changed
+35
-4
No files matched your search
@@ -28,6 +28,7 @@
|
||||
*/
|
||||
|
||||
import { Readable } from 'node:stream';
|
||||
import v8 from 'node:v8';
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
@@ -660,6 +661,28 @@ describe('GeminiTTSProvider.synthesize audio output', () => {
|
||||
expect(wav.subarray(44)).toEqual(pcm);
|
||||
});
|
||||
|
||||
// V8 stops optimizing regexps once a process holds enough compiled code,
|
||||
// as long-running servers do.
|
||||
it('streams audio over 4 MB of base64 with regexp optimization off', async () => {
|
||||
const pcm = Buffer.alloc(4 * 1024 * 1024, 7);
|
||||
replyWith(
|
||||
interaction({ content: [audioBlock(pcm, 'audio/L16;rate=24000')] }),
|
||||
);
|
||||
|
||||
v8.setFlagsFromString('--no-regexp-optimization');
|
||||
let wav: Buffer;
|
||||
try {
|
||||
const result = (await withTestActor(() =>
|
||||
makeProvider().synthesize({ text: 'hi' }),
|
||||
)) as { stream: Readable };
|
||||
wav = await readAll(result.stream);
|
||||
} finally {
|
||||
v8.setFlagsFromString('--regexp-optimization');
|
||||
}
|
||||
expect(wav.length).toBe(44 + pcm.length);
|
||||
expect(wav.subarray(44).equals(pcm)).toBe(true);
|
||||
});
|
||||
|
||||
it('wraps PCM without a declared rate at 24 kHz', async () => {
|
||||
replyWith(
|
||||
interaction({
|
||||
|
||||
@@ -154,7 +154,17 @@ interface Interaction {
|
||||
|
||||
const WAV_MIME_TYPES = new Set(['audio/wav', 'audio/x-wav', 'audio/wave']);
|
||||
const PCM_MIME_TYPES = new Set(['audio/l16', 'audio/pcm']);
|
||||
const BASE64_PATTERN = /^[A-Za-z0-9+/_-]+={0,2}$/;
|
||||
// Find a bad character instead of matching `^[...]+$`: when V8 compiles a
|
||||
// regexp unoptimized, the anchored loop backtracks per character and overflows
|
||||
// its stack on audio over ~4 MB.
|
||||
const NON_BASE64_CHAR = /[^A-Za-z0-9+/_-]/;
|
||||
|
||||
const isBase64 = (data: string): boolean => {
|
||||
let end = data.length;
|
||||
if (data.endsWith('==')) end -= 2;
|
||||
else if (data.endsWith('=')) end -= 1;
|
||||
return end > 0 && !NON_BASE64_CHAR.test(data.slice(0, end));
|
||||
};
|
||||
|
||||
const SPLIT_TEXT_HINT = 'Split the text into shorter requests.';
|
||||
|
||||
@@ -526,7 +536,7 @@ export class GeminiTTSProvider implements ITTSProvider {
|
||||
const { data, mime_type: mimeType } = audio;
|
||||
if (
|
||||
typeof data !== 'string' ||
|
||||
!BASE64_PATTERN.test(data) ||
|
||||
!isBase64(data) ||
|
||||
typeof mimeType !== 'string'
|
||||
) {
|
||||
throw upstreamFailure('Gemini TTS returned invalid audio data');
|
||||
|
||||
Binary file not shown.
@@ -71,7 +71,6 @@ export interface AppIconHostConfig extends TrustedIconHostConfig {
|
||||
pub_port?: number;
|
||||
}
|
||||
|
||||
const RAW_BASE64_REGEX = /^[A-Za-z0-9+/]+={0,2}$/;
|
||||
// `data:<type>/<subtype>[;param[=value]]…,<payload>`. The parameter list is
|
||||
// matched as a group of its own so it can be checked exhaustively — the
|
||||
// previous prefix-scan only looked at the bytes before the first `;` or `,`
|
||||
@@ -185,7 +184,6 @@ function sniffRawBase64Image(
|
||||
if (typeof value !== 'string') return null;
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.length < 16) return null;
|
||||
if (!RAW_BASE64_REGEX.test(trimmed)) return null;
|
||||
const bytes = decodeStrictBase64(trimmed);
|
||||
if (!bytes) return null;
|
||||
const mime = sniffImageMime(bytes);
|
||||
|
||||
Reference in new issue
Block a user