Skip to content

EXC_BREAKPOINT (SIGTRAP) crash in main process — V8 string allocation failure in stream data handlers #7507

@AE-33

Description

@AE-33

Bug: EXC_BREAKPOINT (SIGTRAP) crash on macOS ARM64

Describe the bug

The Goose desktop app crashes with EXC_BREAKPOINT (SIGTRAP) on the main thread (CrBrowserMain) after ~29 minutes of use. The crash is a V8 CHECK assertion failure (brk #0) triggered by Buffer.toString() calls inside Node.js stream 'data' event handlers.

Root Cause

Calling data.toString() inside stream 'data' event handlers on the Electron main thread triggers:

Buffer.toString('utf8')
→ node::StringBytes::Encode()
→ v8::String::NewFromUtf8()
→ V8 NEW_STRING macro → .ToHandleChecked()
→ CHECK fails → FATAL → brk #0 → EXC_BREAKPOINT

When V8 cannot allocate a new string (GC pressure, near-OOM), NewFromUtf8 returns empty MaybeLocal, and .ToHandleChecked() hits a CHECK → crash.

3 vulnerable code paths:

  1. goosed.ts:295-309data.toString() on goosed stdout/stderr (runs continuously)
  2. main.ts:1470-1475data.toString() on ps|grep for check-ollama
  3. main.ts:1527-1532data.toString() on cat for read-file handler

Fix (patch included below)

  1. goosed.ts: Batch raw Buffer chunks, flush via setImmediate() — moves toString() out of the libuv I/O callback
  2. main.ts check-ollama: Accumulate Buffer[], convert on 'close' after stream ends
  3. main.ts read-file: Replace spawn('cat') with fs.readFile() on all platforms

Crash signature

Exception Type:  EXC_BREAKPOINT (SIGTRAP)
Exception Codes: 0x0000000000000001, 0x0000000112b6cef0
ESR: 0xf2000000 (Breakpoint) brk 0

Thread 0 Crashed: CrBrowserMain
0  Electron Framework  ares_dns_rr_get_ttl + 3711128
...
29 node::StringBytes::Encode + 272
30 node::Buffer::RegisterExternalReferences + 25908

Environment

  • OS & Arch: macOS 26.3 (25D125), ARM64 (Mac15,13 — MacBook Air M3)
  • Interface: UI (Desktop)
  • Version: 1.25.0-block.202602182139-cd798
  • Electron: 40.4.0 / Node 24.13.0 / V8 14.4.258.24

Patch

ui/desktop/src/goosed.ts — defer toString() via setImmediate

Replace lines 295-309:

  // OLD (crashes):
  goosedProcess.stdout?.on('data', (data: Buffer) => {
    logger.info(`goosed stdout for port ${port} and dir ${workingDir}: ${data.toString()}`);
  });
  goosedProcess.stderr?.on('data', (data: Buffer) => {
    const lines = data.toString().split('\n');
    ...
  });

With:

  let stdoutPending: Buffer[] = [];
  let stderrPending: Buffer[] = [];
  let stdoutFlushScheduled = false;
  let stderrFlushScheduled = false;

  goosedProcess.stdout?.on('data', (data: Buffer) => {
    stdoutPending.push(Buffer.isBuffer(data) ? data : Buffer.from(data));
    if (!stdoutFlushScheduled) {
      stdoutFlushScheduled = true;
      setImmediate(() => {
        stdoutFlushScheduled = false;
        if (stdoutPending.length > 0) {
          const text = Buffer.concat(stdoutPending).toString('utf8');
          stdoutPending = [];
          logger.info(`goosed stdout for port ${port} and dir ${workingDir}: ${text}`);
        }
      });
    }
  });

  goosedProcess.stderr?.on('data', (data: Buffer) => {
    stderrPending.push(Buffer.isBuffer(data) ? data : Buffer.from(data));
    if (!stderrFlushScheduled) {
      stderrFlushScheduled = true;
      setImmediate(() => {
        stderrFlushScheduled = false;
        if (stderrPending.length > 0) {
          const text = Buffer.concat(stderrPending).toString('utf8');
          stderrPending = [];
          const lines = text.split('\n');
          for (const line of lines) {
            if (line.trim()) {
              errorLog.push(line);
              if (isFatalError(line)) {
                logger.error(`goosed stderr for port ${port} and dir ${workingDir}: ${line}`);
              }
            }
          }
        }
      });
    }
  });

ui/desktop/src/main.ts check-ollama — accumulate Buffers

Replace let output = ''; let errorOutput = ''; and the data handlers with:

      const outputChunks: Buffer[] = [];
      const errorChunks: Buffer[] = [];
      ps.stdout.pipe(grep.stdin);
      grep.stdout.on('data', (data: Buffer) => {
        outputChunks.push(Buffer.isBuffer(data) ? data : Buffer.from(data));
      });
      grep.stderr.on('data', (data: Buffer) => {
        errorChunks.push(Buffer.isBuffer(data) ? data : Buffer.from(data));
      });
      grep.on('close', (code) => {
        const output = Buffer.concat(outputChunks).toString('utf8');
        const errorOutput = Buffer.concat(errorChunks).toString('utf8');
        // ... rest unchanged

ui/desktop/src/main.ts read-file — use fs.readFile on all platforms

Replace the entire read-file handler with:

ipcMain.handle('read-file', async (_event, filePath) => {
  try {
    const expandedPath = expandTilde(filePath);
    const buffer = await fs.readFile(expandedPath);
    return { file: buffer.toString('utf8'), filePath: expandedPath, error: null, found: true };
  } catch (error) {
    console.error('Error reading file:', error);
    return { file: '', filePath: expandTilde(filePath), error, found: false };
  }
});

Full git patch is available at: https://github.com/AE-33/goose (branch: fix/electron-stream-crash-exc-breakpoint)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions