Fix #5621: TypeError from constant-shadowed Hash check in Process.spawn wrapper#5773
Conversation
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.
|
✨ Fix all issues with BitsAI or with Cursor
|
BenchmarksBenchmark execution time: 2026-05-18 20:38:31 Comparing candidate commit 4872962 in PR branch Found 0 performance improvements and 0 performance regressions! Performance is the same for 46 metrics, 0 unstable metrics.
|
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.
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.
…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]>
What does this PR do?
Fixes #5621 by qualifying the env-detection check in the
Process.spawnmonkey patch with::Hashinstead of bareHash.Motivation:
Datadog::Core::Utils::SpawnMonkeyPatch.inject_lineage_envsdecides whether the caller passed an env-Hash via:Because the module is nested under
Datadog::Core::UtilsandDatadog::Core::Utils::Hashexists as a refinement module, bareHashresolves throughModule.nestingto that module — not::Hash— oncecore/utils/hash.rbis loaded (which happens transitively fromrequire 'datadog'). The expressionHash === some_real_hashthen silently returnsfalse.Consequence: even when the caller passes an env-Hash as the first positional arg,
env_providedisfalse. The function takes the no-env branch and prependsDATADOG_ENV.to_h. The caller's env-Hash then occupies the positionProcess.spawnexpects to be the command (a String) —Process.spawnraisesTypeError: no implicit conversion of Hash into Stringfrom inside the wrapper'ssuper.Verified (gist trace) by instrumenting
inject_lineage_envsto log its return value:new_argsstarts with the fullDATADOG_ENVfollowed 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 Stringraised byProcess.spawnwhen 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_provider→env_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 ("
**optscombined with baresupermangles 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 thedef spawn(*args, **opts); super; endsignature unchanged and still fixes the bug.How to test the change?
13 new regression specs in
spec/datadog/core/utils/spawn_monkey_patch_spec.rbcover every distinct calling shape named in the issue:spawn(cmd),spawn(cmd, kw: ...),spawn(cmd, options_hash)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)Each test runs in
expect_in_forkfor isolation and asserts:Process.spawnreturns an Integer pid (no exception insuper)Local verification:
🤖 Generated with Claude Code