Summary
The cp function in node/task.ts uses a regex to detect glob patterns in the source path:
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 [email protected]), 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:
cp(source, dest, '-r') — source contains @
hasGlobPattern regex matches @ → enters glob branch
findMatch(sourceDir, [basename]) resolves the path back to the same literal file (because @ is not actually a glob)
cp(resolvedPath, dest, ...) is called recursively
- The resolved path still contains
@ → goto step 2
- Infinite recursion →
RangeError: Maximum call stack size exceeded
The relevant code (node/task.ts):
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 (which this codebase uses). The @ and # should be removed from the regex.
Introduced By
- PR #1154 — "Preseve symlinks in cp" (commit
f7430e94, 2026-03-06) — introduced the hasGlobPattern regex with @ and #
- PR #1079 — "Remove the shelljs dependency" (commit
4380c120, 2025-02-05) — rewrote cp from scratch, replacing the shelljs implementation
Minimal Reproduction
import * as tl from 'azure-pipelines-task-lib/task';
// Any path containing @ will trigger the bug
tl.cp('/home/[email protected]/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., [email protected]):
Caching tool: helm 4.1.4 x64
##[error]RangeError: Maximum call stack size exceeded
The temp path passed to cp is:
/home/[email protected]/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:
- 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:
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)
Summary
The
cpfunction innode/task.tsuses a regex to detect glob patterns in the source path: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[email protected]), thecpfunction incorrectly enters the glob-handling branch, which callsfindMatch()and then recursively callscp()with the same resolved path, causing infinite recursion:Root Cause
The call chain is:
cp(source, dest, '-r')—sourcecontains@hasGlobPatternregex matches@→ enters glob branchfindMatch(sourceDir, [basename])resolves the path back to the same literal file (because@is not actually a glob)cp(resolvedPath, dest, ...)is called recursively@→ goto step 2RangeError: Maximum call stack size exceededThe relevant code (
node/task.ts):Only
*,?,{,[, and!are meaningful glob characters in minimatch (which this codebase uses). The@and#should be removed from the regex.Introduced By
f7430e94, 2026-03-06) — introduced thehasGlobPatternregex with@and#4380c120, 2025-02-05) — rewrotecpfrom scratch, replacing the shelljs implementationMinimal Reproduction
Real-World Impact
This bug breaks any Azure Pipelines task that caches tools (via
azure-pipelines-tool-lib'scacheDir→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.,[email protected]):The temp path passed to
cpis: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:@and#have no special meaning in minimatch/glob. The existing test suite innode/test/cp.tsdoes not test paths containing@, which is likely why this wasn't caught.A test case should also be added:
Environment
f7430e94)@tl.cpis affected