Copilot memory tool: str_replace with empty old_str causes an infinite loop (100% CPU, unbounded memory growth) until RangeError
- 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
Assessment
This issue has not been assessed yet.