Skip to content

DI: reject method probes on Kernel#lambda#5954

Merged
p-datadog merged 3 commits into
masterfrom
di-reject-kernel-lambda-probes
Jun 25, 2026
Merged

DI: reject method probes on Kernel#lambda#5954
p-datadog merged 3 commits into
masterfrom
di-reject-kernel-lambda-probes

Conversation

@p-datadog

@p-datadog p-datadog commented Jun 25, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Rejects method probes that target Kernel#lambda. A probe whose target
type resolves to the top-level Kernel module and whose method is lambda
now raises Error::ProbeTargetForbidden at hook time instead of being
installed.

Companion to #5560.

Motivation

The method-probe wrapper invokes the original method via super(&block).
On Ruby 3.3+ the original Kernel#lambda raises ArgumentError when it is
reached this way with a non-literal block, so a probe on Kernel#lambda
would break every lambda { ... } call site that passes a captured block.
This is the customer-facing limitation documented in #5560 ("On Ruby 3.3+,
method probes on Kernel#lambda may raise ArgumentError when the probed
call site passes a non-literal block").

There is no legitimate customer use case for probing lambda creation, and
the failure mode is silent and pervasive (every block-passing lambda call
in the process). Rejecting at hook time surfaces an immediate ERROR
status back to the backend so the user sees the problem in the Live
Debugger UI rather than wondering why their application started raising
ArgumentError.

This mirrors #5907 (reject probes on the Datadog namespace): both refuse a
class of method probes that cannot work, using the same
Error::ProbeTargetForbidden. It is orthogonal to #5560 — that PR makes
stdlib probes safe in general; this PR refuses the one stdlib target that
remains broken at a language layer below the tracer's control. Either can
land first.

Change log entry

Yes. Dynamic Instrumentation: reject method probes that target
Kernel#lambda, which cannot be instrumented safely.

Implementation

Instrumenter#hook_method resolves the target class, then rejects the
probe when probe.method_name == "lambda" and the resolved method's owner
is Kernel. Because every class inherits Kernel#lambda, this covers the
Kernel module named directly (including the ::Kernel, Object::Kernel,
and chained Object::Object::Kernel alias forms that Object.const_get
resolves to the same module) as well as inherited targets such as
Object#lambda and String#lambda — a probe naming any type that does
not override lambda resolves to Kernel#lambda and is rejected. A type
that defines its own lambda has a different method owner and falls
through to normal class resolution as an ordinary user method.

Owner resolution replaces an earlier textual Kernel name match, which
missed inherited targets: a probe on Object#lambda named a type whose
name was not Kernel, so it bypassed the textual check and reached the
same broken super(&block) path.

The error reuses Error::ProbeTargetForbidden (added in #5907) and flows
through ProbeManager#add_probe via the existing rescue => exc branch —
it is recorded as a failed probe and an ERROR status is sent to the
backend without any further changes to the manager or notifier.

Line probes are unaffected: they install via TracePoint and do not carry a
type_name/method_name, so the rejection branch is unreachable for them.

How to test the change?

bundle exec rspec spec/datadog/di/instrumenter_spec.rb

105 examples, 0 failures, 8 pending (the same 8 Ruby-2-only pending tests as
on master).

The new specs live under describe '.hook_method'
context 'when targeting Kernel#lambda' and cover:

  • 'Kernel', '::Kernel', 'Object::Kernel', '::Object::Kernel',
    'Object::Object::Kernel' with method lambda — the Kernel module named
    directly and via aliases, all rejected with ProbeTargetForbidden
  • 'Object' and 'String' with method lambda — inherited
    Kernel#lambda targets, also rejected with ProbeTargetForbidden
  • 'Kernel' with a non-lambda method — not rejected (prepend stubbed so
    the example does not install onto the real Kernel)
  • 'KernelLike' with method lambda — not defined here; falls through to
    DITargetNotDefined as expected
  • a class that defines its own lambda — not rejected; the user-defined
    method is instrumented and still returns its value

The method-probe wrapper invokes the original method via super(&block).
On Ruby 3.3+ the original Kernel#lambda raises ArgumentError when reached
this way with a non-literal block, so a probe on Kernel#lambda would break
every lambda { ... } call site that passes a captured block. Probing lambda
creation has no legitimate customer use case.

Reject such probes at hook time with Error::ProbeTargetForbidden, the same
error 5907 uses for Datadog-namespace targets. The check is textual on the
probe's declared type name and method name and runs before class resolution,
tolerating the ::Kernel and Object::Kernel alias forms that const_get
resolves to the top-level Kernel module.

Companion to #5560.
@dd-octo-sts dd-octo-sts Bot added the debugger Live Debugger (+Dynamic Instrumentation, +Symbol Database) label Jun 25, 2026
@dd-octo-sts

dd-octo-sts Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Typing analysis

Note: Ignored files are excluded from the next sections.

steep:ignore comments

This PR introduces 4 steep:ignore comments, and clears 4 steep:ignore comments.

steep:ignore comments (+4-4)Introduced:
lib/datadog/di/instrumenter.rb:154
lib/datadog/di/instrumenter.rb:377
lib/datadog/di/instrumenter.rb:379
lib/datadog/di/instrumenter.rb:702
Cleared:
lib/datadog/di/instrumenter.rb:135
lib/datadog/di/instrumenter.rb:358
lib/datadog/di/instrumenter.rb:360
lib/datadog/di/instrumenter.rb:683

@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented Jun 25, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 0.00%
Overall Coverage: 90.03% (-0.01%)

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

@pr-commenter

pr-commenter Bot commented Jun 25, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-06-25 19:10:12

Comparing candidate commit fae2565 in PR branch di-reject-kernel-lambda-probes with baseline commit 05d39cb in branch master.

Found 0 performance improvements and 0 performance regressions! Performance is the same for 48 metrics, 1 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 ----------------------------------'

@p-datadog
p-datadog marked this pull request as ready for review June 25, 2026 14:56
@p-datadog
p-datadog requested a review from a team as a code owner June 25, 2026 14:56
@p-datadog
p-datadog requested a review from Copilot June 25, 2026 14:56
@p-datadog

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0e9f24869

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/datadog/di/instrumenter.rb Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Dynamic Instrumentation (DI) method-probe hook path to proactively reject probes targeting Kernel#lambda, since wrapping Kernel#lambda is not safe on Ruby 3.3+ (can raise ArgumentError when reached via super(&block) from a wrapper). This prevents silently breaking lambda { ... } call sites and surfaces an immediate ERROR status for the probe.

Changes:

  • Add an early Instrumenter#hook_method guard that raises Error::ProbeTargetForbidden for probes targeting Kernel#lambda (including ::Kernel / Object::Kernel alias forms).
  • Introduce a KERNEL_TYPE_NAME matcher + kernel_type_name? helper for textual detection of the top-level Kernel module name.
  • Add RSpec coverage for the forbidden target, including alias forms and non-matching cases.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
lib/datadog/di/instrumenter.rb Rejects Kernel#lambda method probes at hook time using a new kernel_type_name? helper.
spec/datadog/di/instrumenter_spec.rb Adds specs ensuring Kernel#lambda probes (and alias forms) are rejected with ProbeTargetForbidden.
sig/datadog/di/instrumenter.rbs Updates RBS to include KERNEL_TYPE_NAME and kernel_type_name?.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Strech Strech left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex part is still valid

Comment thread spec/datadog/di/instrumenter_spec.rb Outdated
p-ddsign added 2 commits June 25, 2026 14:41
Address review comment from Strech: the `shared_examples 'rejects the
probe'` block wrapped a single `it` and was invoked five times, adding
indirection without saving anything. Replace it with a plain `.each`
loop over the alias forms, matching how other DI specs parameterize
cases (serializer_spec, utils_spec, redactor_spec).

No behavior change — the same five alias forms are still asserted to
raise ProbeTargetForbidden.

- spec/datadog/di/instrumenter_spec.rb: shared_examples -> .each loop
Address Codex review comment (endorsed by Strech): the previous guard
matched only textual "Kernel" aliases, but hook_method installs probes on
inherited instance methods too. Every class inherits Kernel#lambda, so a
probe with type_name "Object" (or "String", or any class that does not
override lambda) bypassed the textual check, prepended the wrapper ahead
of the inherited Kernel#lambda, and reached the same super(&block) path.
On Ruby 3.3+ that path raises ArgumentError for every lambda { ... } call
site passing a captured block.

Resolve the target method's owner instead of matching the type name:
reject when method_name == "lambda" and the owner is Kernel. This covers
inherited targets and leaves classes that define their own lambda alone.
Remove the now-dead KERNEL_TYPE_NAME constant and kernel_type_name?
method (and their RBS signatures).

Verified on Ruby 3.2.3:
- Object/String#lambda probes now raise ProbeTargetForbidden (fail-before:
  nothing was raised; pass-after: rejected).
- A class overriding lambda is not rejected and the user method runs.
- Full instrumenter_spec.rb: 105 examples, 0 failures, 8 pending.
- standardrb and steep clean on the changed files.

The Ruby 3.3+ ArgumentError itself was not reproduced locally (Ruby
3.2.3); the rejection is version-independent.

- lib/datadog/di/instrumenter.rb: owner-based rejection; drop textual check
- sig/datadog/di/instrumenter.rbs: drop KERNEL_TYPE_NAME / kernel_type_name?
- spec/datadog/di/instrumenter_spec.rb: add Object/String + override cases
@p-datadog

Copy link
Copy Markdown
Member Author

Indeed it was — Object#lambda (and any class that inherits Kernel#lambda without overriding it) slipped past the textual name check. Now rejected by resolving the target method's owner instead of matching the type name, so inherited targets are covered too. Fixed in fae2565.

@p-datadog
p-datadog merged commit 8d31e2c into master Jun 25, 2026
588 checks passed
@p-datadog
p-datadog deleted the di-reject-kernel-lambda-probes branch June 25, 2026 20:10
@dd-octo-sts dd-octo-sts Bot added this to the 2.37.0 milestone Jun 25, 2026
@eregon

eregon commented Jun 29, 2026

Copy link
Copy Markdown
Member

Nice

@p-datadog

Copy link
Copy Markdown
Member Author

Verified manually:
2026-06-30_13-19

p-datadog pushed a commit that referenced this pull request Jun 30, 2026
Conflict in lib/datadog/di/instrumenter.rb resolved by keeping the branch's
refactored hook_method (RubyVersion split, extracted run_method_probe,
re-entrancy guard, DI.invoke_proc) and folding in master's changes since the
merge-base:

- require_relative 'fatal_exceptions' and Datadog::DI.reraise_if_fatal in all
  catch-all rescues (run_method_probe and line_trace_point_callback).
- The #5954 Kernel#lambda probe rejection, via target_method&.owner == ::Kernel.

Kernel#lambda probing is prohibited, so the branch's stdlib integration test
that installed a Kernel#lambda probe was removed; master's rejection tests in
instrumenter_spec.rb cover the prohibition.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

debugger Live Debugger (+Dynamic Instrumentation, +Symbol Database)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants