Skip to content

Debugger external-memory func-eval results can expose stale cached fields after GC #135251

Description

@jkoritzinsky

Description

Current main supports debugger function-evaluation results backed by GC-tracked external memory, including byref-like value types that cannot be boxed. The Left Side transfers the DebuggerExternalMemoryOwner to the Right Side, where CordbVCObjectValue::Init reads the external buffer once into m_pObjectCopy.

The external buffer remains live and its embedded object references/byrefs are updated by ExternalMemoryHandle during GC. However, CordbVCObjectValue::SetExternalMemoryOwner only stores the owner; subsequent GetValue and GetFieldValueForType calls continue reading the original m_pObjectCopy. After a moving GC, the authoritative external buffer can therefore contain relocated references while the debugger exposes stale cached bytes and nested fields.

This is distinct from ordinary CordbVCObjectValue snapshot semantics: ordinary values are neutered/recreated across resumes, while externally owned values intentionally survive and must remain coherent with their GC-updated backing storage.

Reproduction Steps

A deterministic debugger-harness test can reproduce the problem as follows:

  1. Stop a debuggee and function-evaluate a method returning a byref-like value type whose fields include an object reference or managed byref.
  2. Call ICorDebugEval::GetResult and retain the resulting ICorDebugValue / ICorDebugObjectValue.
  3. Trigger a compacting GC, for example through a subsequent function evaluation, while the external-memory owner remains alive.
  4. Read the retained result again through GetValue and/or GetFieldValue.
  5. Compare those values with the current bytes in the Left Side external buffer.

This was observed during managed func-eval validation: after a moving GC, the native external buffer contained the relocated values, but the existing CordbVCObjectValue continued returning its pre-GC cached bytes/fields. A small public standalone repro is not currently available because deterministic timing and result retention require the debugger test harness.

Expected behavior

Externally backed debugger values should reflect the current GC-updated external buffer whenever their bytes or fields are read. Nested value-type field views should inherit the same external-memory behavior.

Actual behavior

CordbVCObjectValue exposes the snapshot captured by Init, even after GC updates the external buffer. Object references or managed byrefs in the cached copy can therefore be stale.

Regression?

This appears to have been introduced with debugger external-memory result ownership. It is present in current main as inspected at e0bb657a25900b08558449f05b3ab059a2330c20. The exact introducing commit has not been isolated.

Known Workarounds

Avoid retaining or reusing externally backed byref-like func-eval results across any operation that can trigger GC. Re-running the evaluation may produce a fresh value, but this is not a reliable general workaround.

Configuration

Observed while validating a Windows x64 Checked runtime build. The affected Right Side caching and external-memory ownership code is shared debugger logic and does not appear inherently Windows- or x64-specific.

Other information

Relevant current-main flow:

  • Debugger::FuncEvalComplete publishes m_externalMemoryOwner with the result and transfers ownership after the completion event.
  • CordbEval::GetResult passes the owner to CordbValue::CreateValueByType and transfers it to the unboxed CordbVCObjectValue.
  • CordbVCObjectValue::Init copies the complete remote value into m_pObjectCopy once.
  • CordbVCObjectValue::SetExternalMemoryOwner stores ownership but does not mark the cache as refreshable.
  • CordbVCObjectValue::GetValue and GetFieldValueForType read m_pObjectCopy without first re-reading the external value home.

A fix should preserve ordinary snapshot behavior while marking externally owned CordbVCObjectValue instances as external-backed and refreshing m_pObjectCopy from m_pValueHome before exposing bytes or fields. Nested value-type field values should carry that marker as well, so nested field access refreshes from the authoritative external storage.

Related debugger func-eval lifetime issues #135247 and #135249 are separate problems.

Note

This issue was drafted with GitHub Copilot.

Activity

  1. added theissue type on Oct 5, 2026
  2. dotnet-policy-service commented on Oct 5, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
    See info in area-owners.md if you want to be subscribed.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions