Skip to content

require-sync-exec-timeout: options-object built entirely from a spread parameter with no timeout anywhere silently escapes detec #54735

Description

@github-actions

Summary

require-sync-exec-timeout (eslint-factory/src/rules/require-sync-exec-timeout.ts) treats any SpreadElement inside the options object as "caller may already have included a timeout" and bails out (hasTimeoutOption returns true the moment it sees prop.type === SpreadElement). This is a reasonable default to avoid false positives when the spread source is genuinely unknown/external, but it also silently clears calls where the spread source is a same-file wrapper parameter that is provably never given a timeout, leaving a real hang risk unflagged.

Grounded false negative

actions/setup/js/build_checkout_manifest.cjs:

42: function resolveDefaultBranch(repository, checkoutPath, options = {}) {
...
46:    ((args, execOptions = {}) => {
47:      try {
48:        return execFileSync("git", args, { encoding: "utf8", ...execOptions });
...
55:    ((args, execOptions = {}) => {
56:      try {
57:        return execFileSync("gh", args, {
58:          encoding: "utf8",
59:          env: { ...process.env, ...(execOptions.env || {}) },
60:          ...execOptions,
61:        });
...
71:      const output = runGit(["-C", repoPath, "symbolic-ref", "--short", "refs/remotes/origin/HEAD"], {
72:        stdio: ["ignore", "pipe", "pipe"],
73:      });
...
84:      const ghExecOptions = { stdio: ["ignore", "pipe", "pipe"] };
88:      if (checkoutToken !== "") ghExecOptions.env = { GH_TOKEN: checkoutToken };
90:      defaultBranch = runGH(["api", `repos/${repository}`, "--jq", ".default_branch"], ghExecOptions).trim();

Neither the runGit call (line 71, options { stdio: [...] }) nor the runGH call (line 90, ghExecOptions = { stdio: [...] } or { stdio: [...], env }) ever supplies a timeout. Both flow into execFileSync("git"/"gh", args, { encoding: "utf8", ...execOptions }), so the final options object genuinely has no timeout key at either call site -- unlike every other execSync/execFileSync/spawnSync call in actions/setup/js (git_helpers.cjs, apply_samples.cjs, merge_remote_agent_github_folder.cjs, memory_custom_validation.cjs, artifact_client.cjs, safeoutputs_cli.cjs, start_mcp_gateway.cjs, evaluate_outcomes.cjs), which all hardcode a positive default timeout. A stalled git symbolic-ref (lock contention) or gh api repos/... call (network stall) here can hang indefinitely with no actionable diagnostic -- exactly the failure mode the rule's own description targets.

The rule never reaches this because execFileSync("git"/"gh", args, { encoding: "utf8", ...execOptions }) contains a SpreadElement (...execOptions), so hasTimeoutOption returns true unconditionally without looking at what execOptions actually is at any call site.

Acceptance criteria

  • When the options object's only non-timeout content is a single SpreadElement whose argument is an Identifier resolving to a function parameter with a literal default value (= {} or an ObjectExpression default with no timeout key), and that parameter is never reassigned before the spread, do not treat the spread as sufficient by itself -- instead check the resolved call-site arguments passed for that parameter across the file (or, at minimum, stop treating a bare {}-defaulted parameter spread as a free pass) so build_checkout_manifest.cjs:48/:57 (and their callers at :71/:90) are flagged.
  • Keep the existing conservative behavior for spreads whose source cannot be statically characterized (e.g. spreads of an untyped external parameter with no default, or of ...process.env-shaped values) to avoid new false positives.
  • Add a RuleTester invalid case shaped like the wrapper-closure pattern above: a helper (args, execOptions = {}) => execFileSync("git", args, { encoding: "utf8", ...execOptions }) called with an options object containing no timeout.
  • Re-verify existing valid cases using spreads from genuinely-unresolvable sources still pass (no new false positives).
  • Once the rule catches this, add timeout: <positive ms> (matching the child_process_timeouts.cjs/GIT_COMMAND_TIMEOUT_MS-style convention used elsewhere) to the two call sites in build_checkout_manifest.cjs as a follow-up fix.

Scope

eslint-factory/src/rules/require-sync-exec-timeout.ts (+ its .test.ts). Rule target file: actions/setup/js/build_checkout_manifest.cjs.

Generated by 🤖 ESLint Refiner · agent · 197.6 AIC · ⌖ 16 AIC · ⊞ 5.8K ·

  • expires on Aug 28, 2026, 9:28 PM UTC-08:00

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions