Skip to content

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

@Ludde321

Description

@Ludde321

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:

  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 recursionRangeError: 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 cacheDirtl.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)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions