microsoft / microsoft/vscode

Copilot memory tool: str_replace with empty old_str causes an infinite loop (100% CPU, unbounded memory growth) until RangeError

Open
#333,395 0 comments 0 reactions 1 assignee Claimed by @roblourens View on GitHub
chat-tools
Dominant language
TypeScript
Stars
193k
Forks
42.4k
PR merge metrics
PR metrics pending

Description

## Summary

The bundled Copilot **memory tool** enters an **infinite loop** when `str_replace` is called with an **empty `old_str`**: it pins a CPU core at 100%, grows an array without bound, and only terminates when V8 throws `RangeError: Invalid array length`. In a real session this froze the chat for **~21 minutes** before the tool failed.

## Affected code

`extensions/copilot/src/extension/tools/node/memoryTool.tsx` (`_localStrReplace`, current `main`):

https://github.com/microsoft/vscode/blob/main/extensions/copilot/src/extension/tools/node/memoryTool.tsx#L606-L618

```ts
const occurrences: number[] = [];
let searchStart = 0;
while (true) {
const idx = content.indexOf(params.old_str, searchStart);
if (idx === -1) {
break; // never taken when old_str === ""
}
const lineNumber = content.substring(0, idx).split('\n').length;
occurrences.push(lineNumber); // grows without bound
searchStart = idx + 1;
}
```

## Root cause

`String.prototype.indexOf('', pos)` **never returns `-1`**: an empty search string matches at every position and the spec returns `Math.min(pos, content.length)`.

```bash
node -e "const s='abc'; console.log(s.indexOf('', 10), s.indexOf('', 999))"
# 3 3 <- always clamped, never -1
```

Once `searchStart` passes the end of the content, `idx` stays at `content.length` forever, the `idx === -1` guard never fires, and the loop pushes one entry into `occurrences` on every iteration until V8's array length limit is reached → `RangeError: Invalid array length` (with the CPU pinned at 100% and memory growing the whole time).

## How it was triggered (real session, 2026-08-30)

A Copilot Chat agent session called the `memory` tool with `str_replace` and an **empty `old_str`** (the model intended to append a note to `/memories/repo/dev-sidecar-xray.md`). The chat froze for **~21 minutes**, then the tool failed with:

```
Error from tool memory with args {"command":"str_replace","file_text":"","new_str":"- **…**","old_path":"/memories/repo/dev-sidecar-xray.md","old_str":"","path":"/memories/repo/dev-sidecar-xray.md"}: Invalid array length: RangeError: Invalid array length
```

Timeline from the extension logs:

- `19:45:30` — model emits the `memory` tool call, chat turn completes
- `19:45:30 → 20:06:30` — tool call hangs (~21 min, one core at 100%)
- `20:06:30` — `RangeError: Invalid array length` logged, error returned to the model
- `20:06:31` — follow-up request; the model retried with a corrected call and recovered

## Minimal reproduction

```js
// Reproduces the loop from _localStrReplace with an empty old_str
const content = 'x'.repeat(100);
const occurrences = [];
let searchStart = 0;
while (true) {
const idx = content.indexOf('', searchStart); // never -1
if (idx === -1) break;
occurrences.push(idx);
searchStart = idx + 1; // advances, but '' always matches
}
// runs until occurrences.length reaches 2^32-1 -> RangeError: Invalid array length
```

Run it with `node repro.js` — it never terminates on its own and pins one core at 100% while memory grows.

## Suggested fix

Guard against an empty `old_str` before the scan — either reject it with a descriptive error result (consistent with the existing "pattern not found" / "multiple occurrences" error results), or define empty `old_str` as an explicit append/insert operation. A guard also protects against the model emitting an empty string, which does happen in practice (it occurred in a real session, as shown above).

Happy to submit a PR with the guard plus regression tests if this approach sounds good.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.