microsoft / microsoft/azure-pipelines-task-lib

`cp()` treats `@` and `#` in file paths as glob patterns, causing infinite recursion (RangeError: Maximum call stack size exceeded)

Open Beginner friendly
#1,166 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Area: TaskLib triage
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.