microsoft / microsoft/azure-pipelines-task-lib
`cp()` treats `@` and `#` in file paths as glob patterns, causing infinite recursion (RangeError: Maximum call stack size exceeded)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 458
- Forks
- 282
- Avg merge
- 1d 4h
- Merged PRs (30d)
- 14
Description
## Summary
The `cp` function in `node/task.ts` uses a regex to detect glob patterns in the source path:
```typescript
const hasGlobPattern = /[*?{[!@#]/.test(source);
```
**The `@` and `#` characters are not glob pattern characters**, but this regex treats them as such. When a file path contains `@` (very common on Linux — e.g., a service account named `user@domain.com`), the `cp` function incorrectly enters the glob-handling branch, which calls `findMatch()` and then **recursively calls `cp()` with the same resolved path**, causing infinite recursion:
```
RangeError: Maximum call stack size exceeded
```
### Root Cause
The call chain is:
1. `cp(source, dest, '-r')` — `source` contains `@`
2. `hasGlobPattern` regex matches `@` → enters glob branch
3. `findMatch(sourceDir, [basename])` resolves the path back to the **same literal file** (because `@` is not actually a glob)
4. `cp(resolvedPath, dest, ...)` is called recursively
5. The resolved path still contains `@` → goto step 2
6. **Infinite recursion** → `RangeError: Maximum call stack size exceeded`
The relevant code ([`node/task.ts`](https://github.com/microsoft/azure-pipelines-task-lib/blob/5d545c0eb2882e87022007b9acd2f386b9daee21/node/task.ts)):
```typescript
const hasGlobPattern = /[*?{[!@#]/.test(source);
if (hasGlobPattern) {
let sourcesToProcess: string[] = [];
let sourceDir = path.dirname(source);
sourceDir = sourceDir == '.' ? path.resolve() : sourceDir;
sourcesToProcess = findMatch(sourceDir, [path.basename(source)]);
// ...
for (const src of sourcesToProcess) {
cp(src, destination, options as CopyOptionsVariants, continueOnError, retryCount);
// ↑ recursive call with the same path still containing @
}
return;
}
```
Only `*`, `?`, `{`, `[`, and `!` are meaningful glob characters in [minimatch](https://github.com/isaacs/minimatch) (which this codebase uses). The `@` and `#` should be removed from the regex.
### Introduced By
- **PR [#1154](https://github.com/microsoft/azure-pipelines-task-lib/pull/1154)** — *"Preseve symlinks in cp"* (commit [`f7430e94`](https://github.com/microsoft/azure-pipelines-task-lib/commit/f7430e942ba3f351d1756a505e5f6daabe8e1678), 2026-03-06) — introduced the `hasGlobPattern` regex with `@` and `#`
- **PR [#1079](https://github.com/microsoft/azure-pipelines-task-lib/pull/1079)** — *"Remove the shelljs dependency"* (commit [`4380c120`](https://github.com/microsoft/azure-pipelines-task-lib/commit/4380c120cb87bbdc9c2db75da3a33c8bd993446a), 2025-02-05) — rewrote `cp` from scratch, replacing the shelljs implementation
### Minimal Reproduction
```typescript
import * as tl from 'azure-pipelines-task-lib/task';
// Any path containing @ will trigger the bug
tl.cp('/home/user@domain.com/somefile.txt', '/tmp/dest');
// → RangeError: Maximum call stack size exceeded
```
### Real-World Impact
This bug breaks **any Azure Pipelines task that caches tools** (via `azure-pipelines-tool-lib`'s `cacheDir` → `tl.cp`) on self-hosted Linux agents whose service account username contains `@`.
We discovered this because the **HelmInstallerV1** task started failing on our self-hosted Linux agent where the service account username contains `@` (e.g., `svc-agent@contoso.com`):
```
Caching tool: helm 4.1.4 x64
##[error]RangeError: Maximum call stack size exceeded
```
The temp path passed to `cp` is:
```
/home/svc-agent@contoso.com/agent/_work/_temp/helm-v4.1.4-linux-amd64.zip
^ matches the @ in the regex
```
This affects **all tool installations** on that agent (not just Helm) — any tool version that isn't already in the cache will fail. Azure-hosted agents are unaffected because their paths don't contain `@`.
### Suggested Fix
Remove `@` and `#` from the glob detection regex:
```diff
- const hasGlobPattern = /[*?{[!@#]/.test(source);
+ const hasGlobPattern = /[*?{[!]/.test(source);
```
`@` and `#` have no special meaning in minimatch/glob. The existing test suite in `node/test/cp.ts` does not test paths containing `@`, which is likely why this wasn't caught.
A test case should also be added:
```typescript
it('cp handles paths containing @ character', (done) => {
const srcDir = path.resolve(DIRNAME, 'dir@test');
const destDir = path.resolve(DIRNAME, 'dest-at');
tl.mkdirP(srcDir);
fs.writeFileSync(path.join(srcDir, 'file.txt'), 'content');
assert.doesNotThrow(() => tl.cp(path.join(srcDir, 'file.txt'), destDir));
assert.ok(fs.existsSync(path.join(destDir, 'file.txt')));
tl.rmRF(srcDir);
tl.rmRF(destDir);
done();
});
```
### Environment
- **azure-pipelines-task-lib**: latest (post-commit `f7430e94`)
- **Agent**: Self-hosted Linux agent, service account username containing `@`
- **Task**: HelmInstallerV1 (v1.272.0) — but any task using `tl.cp` is affected
- **Node**: 20.x (agent-provided)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in node/task.ts at the cp glob-detection branch, then read the existing tests in node/test/cp.ts. Run the cp tests first and add coverage for source paths containing @ and #. Done means literal paths no longer recurse and the copied file is present at the destination.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100