Skip to content

Volta extends resolution in getNodeVersionFromFile has no cycle detection, recursing until stack overflow #1628

Description

@krishna3554

Summary

getNodeVersionFromFile follows Volta extends recursively with no cycle detection, so a self-referential or mutually-referential volta.extends chain causes unbounded recursion and crashes the action with a stack overflow instead of a clear error.

Location

  • File: src/util.ts
  • Function: getNodeVersionFromFile(versionFilePath: string)
  • Code path:
if (manifest.volta?.extends) {
  const extendedFilePath = path.resolve(
    path.dirname(versionFilePath),
    manifest.volta.extends
  );
  core.info('Resolving node version from ' + extendedFilePath);
  return getNodeVersionFromFile(extendedFilePath);
}

No visited-path set or depth limit is threaded through the recursion.

Problem

Volta workspaces support {"volta": {"extends": "./base/package.json"}}. If the target (transitively) points back to a file already on the resolution stack — e.g. package.json with "extends": "./package.json" (typo), or a.json → b.json → a.json — the function recurses forever until V8 throws RangeError: Maximum call stack size exceeded. The user gets an opaque stack-overflow failure rather than a diagnostic naming the cycle.

Trigger / Reproduction

Based on static analysis (no workflow run performed):

  1. Set node-version-file to a package.json containing:
    {"volta": {"extends": "./package.json"}}
    or a two-file cycle a.jsonb.json via volta.extends.
  2. Run the action. getNodeVersionFromFile resolves extends, re-enters itself with the same path, and never terminates normally.

Note: this is a static-analysis finding; I did not execute a workflow against a cyclic fixture.

Expected Behavior

Cyclic volta.extends should fail fast with a clear error naming the files in the cycle (e.g. Detected cyclic volta.extends: a.json -> b.json -> a.json), consistent with how missing files already throw The specified node version file at: ... does not exist.

Actual Behavior

Unbounded recursion until stack exhaustion, producing an unactionable RangeError with no mention of the offending extends chain.

Impact

  • A one-character typo in volta.extends turns a configuration mistake into an opaque action crash, costing debugging time.
  • No data-loss risk, but the failure mode hides the actual cause (cycle) behind a generic engine error.

Suggested Direction

  • Thread a seen: Set<string> (resolved absolute paths) through getNodeVersionFromFile, checking before recursing and throwing a descriptive cycle error. Alternatively cap recursion depth with the same diagnostic. Either preserves current behavior for acyclic chains.

Evidence

  • Source via API: src/util.ts Volta-extends branch shows direct unconditional recursion with no guard; neighboring branches (missing file, non-JSON/TOML fallthrough) all have explicit handling, highlighting the gap.
  • Duplicate check: issue search for volta extends cycle recursion returns total_count: 0, and the open-issue list contains no extends-cycle report (nearest is .nvmrc comment parsing) — no apparent duplicate.

What happened

Unbounded recursion on cyclic Volta extends as detailed above.

Expected behavior

Fast, descriptive cycle error instead of stack overflow.

Steps to reproduce

Point node-version-file at a self- or mutually-referential Volta extends fixture and observe getNodeVersionFromFile re-enter indefinitely (static path; runtime stack overflow implied).

reproducible code

// package.json
{"volta": {"extends": "./package.json"}}
- uses: actions/setup-node@v4
  with:
    node-version-file: package.json

manifest.yaml

N/A

Versions

  • actions/setup-node: current main (verified via API, src/util.ts)
  • Node: N/A (static analysis finding)

Classification

  • FACT: Volta extends recursion has no visited-set or depth cap (verified in source via API).
  • INFERENCE: cyclic fixtures therefore recurse until stack exhaustion.
  • HYPOTHESIS: a visited-path guard produces a clear error with no change to valid chains.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions