Skip to content

Fix #5621: TypeError from constant-shadowed Hash check in Process.spawn wrapper#5773

Merged
p-datadog merged 2 commits into
masterfrom
claude/fix-5621-process-spawn-hash-shadow
May 19, 2026
Merged

Fix #5621: TypeError from constant-shadowed Hash check in Process.spawn wrapper#5773
p-datadog merged 2 commits into
masterfrom
claude/fix-5621-process-spawn-hash-shadow

Conversation

@p-datadog

Copy link
Copy Markdown
Member

What does this PR do?

Fixes #5621 by qualifying the env-detection check in the Process.spawn monkey patch with ::Hash instead of bare Hash.

Motivation:

Datadog::Core::Utils::SpawnMonkeyPatch.inject_lineage_envs decides whether the caller passed an env-Hash via:

env_provided = Hash === args.first

Because the module is nested under Datadog::Core::Utils and Datadog::Core::Utils::Hash exists as a refinement module, bare Hash resolves through Module.nesting to that module — not ::Hash — once core/utils/hash.rb is loaded (which happens transitively from require 'datadog'). The expression Hash === some_real_hash then silently returns false.

Consequence: even when the caller passes an env-Hash as the first positional arg, env_provided is false. The function takes the no-env branch and prepends DATADOG_ENV.to_h. The caller's env-Hash then occupies the position Process.spawn expects to be the command (a String) — Process.spawn raises TypeError: no implicit conversion of Hash into String from inside the wrapper's super.

Verified (gist trace) by instrumenting inject_lineage_envs to log its return value: new_args starts with the full DATADOG_ENV followed by the caller's {"EXTRA"=>"1"} Hash.

Affected callers (named in the issue): childprocess, terrapin, launchy, selenium-webdriver, cuprite/ferrum, danger.

The bug is not Ruby-3.4-specific (despite all reports being on 3.4.x). Reproduces identically on Ruby 3.3.10 and 3.4.8. Users only hit it on 3.4.x because that's what they ship.

Change log entry

Yes. Fix TypeError: no implicit conversion of Hash into String raised by Process.spawn when the application uses gems that pass an environment Hash (e.g. childprocess, terrapin, launchy, selenium-webdriver, cuprite, ferrum, danger).

Additional Notes:

This is a minimal, targeted alternative to #5634, which bundles this fix with other refactors (rename lineage_envs_providerenv_provider, drop **opts, add idempotency guard, doc updates). That PR has stalled with 21 failing CI jobs and unresolved review comments since 2026-05-06. This PR isolates the single change that closes the user complaint.

The reporter's diagnosis in the issue ("**opts combined with bare super mangles the trailing options hash") is incorrect — the root cause is constant resolution, not keyword forwarding. The diagnosis is disproved by the issue reporter's own "neutralized" snippet, which keeps the def spawn(*args, **opts); super; end signature unchanged and still fixes the bug.

How to test the change?

13 new regression specs in spec/datadog/core/utils/spawn_monkey_patch_spec.rb cover every distinct calling shape named in the issue:

  • baseline shapes: spawn(cmd), spawn(cmd, kw: ...), spawn(cmd, options_hash)
  • env-Hash shapes (these all failed on master): spawn(env, cmd), spawn(env, cmd, options_hash) (terrapin), spawn(env, *args, options_hash) (childprocess multi-arg), spawn(env, [cmd, argv0], options_hash) (childprocess single-arg), spawn(env, cmd, options_with_integer_fd_keys) (childprocess close-on-exec), spawn(env, cmd, **opts)
  • behavior: parent ENV reaches child; caller's env-Hash is not mutated

Each test runs in expect_in_fork for isolation and asserts:

  1. Process.spawn returns an Integer pid (no exception in super)
  2. The child exits 0
  3. The injected lineage env var actually reaches the child

Local verification:

  • Ruby 3.3.10: 13/13 pass with fix, 8/13 fail without
  • Ruby 3.4.8: 13/13 pass with fix, 8/13 fail without

🤖 Generated with Claude Code

The wrapper for Process.spawn detected an env-Hash argument with
`Hash === args.first`. Because SpawnMonkeyPatch is nested under
Datadog::Core::Utils and Datadog::Core::Utils::Hash exists as a
refinement module, bare `Hash` resolves to that module via
Module.nesting once it is loaded — so the check silently returned
false for real Hashes.

The wrapper then took the "no env provided" branch and prepended
DATADOG_ENV.to_h, pushing the caller's env-Hash into the command
slot and raising "TypeError: no implicit conversion of Hash into
String" inside Process.spawn.

Reported by users of childprocess, terrapin, launchy,
selenium-webdriver, cuprite/ferrum, and danger.

Fix: use ::Hash to bypass lexical constant resolution.
@p-datadog
p-datadog requested a review from a team as a code owner May 18, 2026 20:11
@p-datadog p-datadog added the AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos label May 18, 2026
@dd-octo-sts dd-octo-sts Bot added the core Involves Datadog core libraries label May 18, 2026
@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented May 18, 2026

Copy link
Copy Markdown

Tests

Fix all issues with BitsAI or with Cursor

⚠️ Warnings

❄️ 1 New flaky test detected

Datadog::Core::Utils::SpawnMonkeyPatch Process.spawn argument forms (issue #5621 regression) spawn(env, cmd, options_with_integer_fd_keys) from rspec   View in Datadog   (Fix with Cursor)
Status:#<Process::Status: pid 8419 exit 1> STDOUT:\`\` STDERR:\`/__w/dd-trace-rb/dd-trace-rb/lib/datadog/core/utils/spawn_monkey_patch.rb:17:in \`spawn': no implicit conversion of Hash into String (TypeError)
	from /__w/dd-trace-rb/dd-trace-rb/lib/datadog/core/utils/spawn_monkey_patch.rb:17:in \`spawn'
	from /__w/dd-trace-rb/dd-trace-rb/spec/datadog/core/utils/spawn_monkey_patch_spec.rb:53:in \`run_spawn'
	from /__w/dd-trace-rb/dd-trace-rb/spec/datadog/core/utils/spawn_monkey_patch_spec.rb:153:in \`block (4 levels) in <top (required)>'
	from /__w/dd-trace-rb/dd-trace-rb/spec/support/synchronization_helpers.rb:28:in \`block in expect_in_fork'
	from /__w/dd-trace-rb/dd-trace-rb/lib/datadog/core/utils/at_fork_monkey_patch.rb:58:in \`block in fork'
	from /usr/local/bundle/gems/ffi-1.17.4/lib/ffi/ffi.rb:69:in \`block in fork'
	from /usr/local/bundle/gems/ffi-1.17.4/lib/ffi/ffi.rb:67:in \`fork'
	from /usr/local/bundle/gems/ffi-1.17.4/lib/ffi/ffi.rb:67:in \`fork'
	from /__w/dd-trace-rb/dd-trace-rb/lib/datadog/core/utils/at_fork_monkey_patch.rb:64:in \`fork'
...

ℹ️ Info

No other issues found (see more)

🧪 All tests passed

🎯 Code Coverage (details)
Patch Coverage: 28.28%
Overall Coverage: 97.08% (-0.07%)

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 2381b8d | Docs | Datadog PR Page | Give us feedback!

@pr-commenter

pr-commenter Bot commented May 18, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-05-18 20:38:31

Comparing candidate commit 4872962 in PR branch claude/fix-5621-process-spawn-hash-shadow with baseline commit 87c5373 in branch master.

Found 0 performance improvements and 0 performance regressions! Performance is the same for 46 metrics, 0 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

Two issues caught by CI:

- standard/lint: hash-literal spacing and `Lint/ConstantDefinitionInBlock`.
  Auto-fixed spacing via `rake standard:fix`; converted the three describe-
  block constants (LINEAGE_VAR / LINEAGE_VAL / PROBE_CMD) into `let` blocks.

- Ruby 2.5/2.6/2.7: the `options_with_integer_fd_keys` test failed because
  the wrapper's `def spawn(*args, **opts)` partially extracts a positional
  Hash with mixed Symbol + Integer keys into `**opts` on Ruby 2.x. That is
  a distinct bug from the constant shadow this PR fixes — it is the bug
  that PR #5774 (drop `**opts`) addresses. The test (and its proof) belong
  with that PR. Removed from this spec with a comment pointing to #5774.
p-datadog pushed a commit that referenced this pull request May 18, 2026
The `**opts` parameter (removed in the previous commit) had a real bug,
not a cosmetic one: on Ruby 2.5/2.6/2.7, `def spawn(*args, **opts)`
partially extracts a positional Hash with mixed Symbol + Integer keys —
the Symbol-keyed entries are siphoned into `**opts` while the
Integer-keyed entries stay positional, mangling the call. The wrapper's
bare `super` then forwards the broken split and `Process.spawn` raises
`TypeError: no implicit conversion of Hash into String`.

Ruby 3+ removed auto-extraction of positional Hashes into kwargs, so
the bug is invisible there.

`childprocess` triggers this shape via `options[writer.fileno] = :close`
on duplex pipes.

The new test does not pass an env-Hash argument, so it is independent
of the constant-shadow fix in PR #5773 — confirmed by reproducing the
TypeError on Ruby 2.5/2.6/2.7 with master's `**opts` signature in place
and `::Hash` not applied.
@p-datadog
p-datadog merged commit f0cfa4f into master May 19, 2026
581 checks passed
@p-datadog
p-datadog deleted the claude/fix-5621-process-spawn-hash-shadow branch May 19, 2026 12:18
@dd-octo-sts dd-octo-sts Bot added this to the 2.34.0 milestone May 19, 2026
@mabdinur mabdinur mentioned this pull request May 19, 2026
p-datadog pushed a commit that referenced this pull request May 19, 2026
…ns Hash bug

Master's wrapper signature `def spawn(*args, **opts)` is unchanged from before.
On Ruby 3+ this works fine. On Ruby 2.5/2.6/2.7 `**opts` auto-extracts the
Symbol-keyed entries from a mixed-keys positional options Hash (e.g.
childprocess's `options[writer.fileno] = :close` on duplex pipes), splitting
the Hash and raising `TypeError: no implicit conversion of Hash into String`
inside `Process.spawn`.

Use a per-Ruby-version signature (matching `lib/datadog/core/utils/forking.rb`):
- Ruby 3+: `def spawn(*args, **opts)` — preserves caller kwargs as kwargs
  through bare `super`
- Ruby 2.5/2.6/2.7: `def spawn(*args)` — keeps the options Hash positional
  and intact regardless of key shape

Adds a regression test (`spawn(cmd, options_hash_with_mixed_symbol_and_integer_keys)`)
under the existing `Process.spawn argument forms (issue #5621 regression)`
describe block, using the shared `run_spawn` helper added in #5773. The test
triggers `TypeError` on master against Ruby 2.5/2.6/2.7 and passes on every
Ruby with the per-version split. No env-Hash is passed, so the test is
independent of the constant-shadow fix in #5773.

Sig stays at `(*untyped args)` to match the 2.x form (RBS doesn't support
runtime conditionals); the Ruby 3+ branch uses an inline
`# steep:ignore DifferentMethodParameterKind` directive.

All 13 spec/datadog/core/utils/spawn_monkey_patch_spec.rb examples pass.
Steep clean.

Co-Authored-By: Claude <[email protected]>
@Strech Strech mentioned this pull request May 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos core Involves Datadog core libraries

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: datadog-2.31.0 monkey-patches Process.spawn and interferes with Ferrum

3 participants