Skip to content

Fix closed generic instance corruption and generic type-param cast#3452

Draft
danielmeza wants to merge 2 commits into
nanoframework:developfrom
danielmeza:fix/generics-closed-instance
Draft

Fix closed generic instance corruption and generic type-param cast#3452
danielmeza wants to merge 2 commits into
nanoframework:developfrom
danielmeza:fix/generics-closed-instance

Conversation

@danielmeza

Copy link
Copy Markdown

Description

  • Guards the object-lock cache against closed generic instances so that entering a Synchronized instance method no longer overwrites the closed TypeSpec stored in the object header.
  • Completes the DATATYPE_GENERICINST handling for the dedicated heap representation (boxed Nullable<T>): garbage-collector reachability marking and heap-compaction relocation now walk its fields, and type recovery (InitializeFromObject/ExtractTypeIndexFromObject), initobj/clone, GetHashCode, ObjectsEqual and Compare_Values all treat it as an inline value type.
  • Recovers the closed TypeSpec during CEE_CALL/CEE_CALLVIRT so a generic type-parameter cast (castclass !0) resolves correctly when the interface call is dispatched within the same assembly.
  • Removes the unconditional NewGenericInstanceObject debug traces.

Motivation and Context

  • This addresses two crashes that affect closed generic class instances under the generics public preview.
  • A closed generic class instance stores its closed TypeSpec in the object-header slot that overlaps the monitor-lock pointer. Entering a Synchronized instance method — which is what a C# field-like event add/remove accessor compiles to — locked this and wrote the lock pointer into that slot, destroying the TypeSpec. Any subsequent generic field or method resolution then dereferenced a wild pointer and faulted (LoadProhibited). FindLockObject had the inverse defect, reading the TypeSpec back as a lock pointer. Monitor ownership still works through the per-thread lock lists, so guarding the cache against generic instances is sufficient.
  • A (T)item cast (castclass !0) inside an explicit interface implementation on a closed generic instance threw CLR_E_INVALID_CAST: the dispatch lost the closed TypeSpec, so the VAR branch of InitializeFromSignatureToken had no generic context. The CALLVIRT generic-context recovery pre-pass was guarded to TBL_MethodRef (cross-assembly interface calls); when the interface and the caller live in the same assembly the call is a TBL_MethodDef, so the pre-pass never ran and the generic type stayed null. The fix prefers the receiver's own stored closed TypeSpec over the caller-assembly TypeSpec-table scan, and adds an else-branch recovery from the receiver for the same-assembly MethodDef interface dispatch the pre-pass misses.

How Has This Been Tested?

  • Validated on an ESP32-S3 device build.
  • A C# field-like event on a closed generic type (Observable<int>.Changed += handler; raise) now subscribes and fires without faulting, where it previously crashed with LoadProhibited.
  • A generic DataTemplate<T> provider whose IDataTemplate.Build(object) performs a (T)item cast, driving a repeater over an ObservableCollection<T> (both in the same assembly), now builds, rebuilds on collection change, and applies per-item bindings with no InvalidCastException.

Types of changes

  • Bug fix (non-breaking change which fixes an issue with code or algorithm)

Checklist:

  • My code follows the code style of this project (only if there are changes in source code).
  • I have read the CONTRIBUTING document.

…GENERICINST handling

A closed generic class instance stores its closed TypeSpec in the object-header
slot that overlaps the monitor-lock pointer. Entering a Synchronized instance
method (which C# field-like event add/remove accessors are) locked `this` and
wrote the lock pointer into that slot, destroying the TypeSpec; subsequent
generic field/method resolution then dereferenced a wild pointer and crashed
(LoadProhibited). FindLockObject had the inverse defect, reading the TypeSpec
back as a lock pointer.

Guards the object-lock cache against generic instances
(CLR_RT_HeapBlock_Lock::CreateInstance/ChangeOwner and FindLockObject); monitor
ownership still works through the per-thread lock lists. Closes the related GC
and type-system gaps for the dedicated DATATYPE_GENERICINST heap representation
(boxed Nullable<T>): GC reachability marking and heap-compaction relocation walk
its fields, type recovery (InitializeFromObject/ExtractTypeIndexFromObject) and
initobj/clone handle it, and NewGenericInstanceObject records the closed TypeSpec
and the generic-instance flag.

Also completes the remaining DATATYPE_GENERICINST handling: GetHashCode,
ObjectsEqual and Compare_Values treat it like a value type (inline fields);
the compactor ValidateCluster overlapping-block check excludes it (its header
slot is reused for object data, like class/value-type); and the unconditional
NewGenericInstanceObject debug traces are removed.
A (T)item cast (castclass !0) inside an explicit interface implementation on a closed generic
instance threw CLR_E_INVALID_CAST: the dispatch lost the closed TypeSpec, so the VAR branch of
InitializeFromSignatureToken had no generic context and failed the cast.

The CALLVIRT generic-context recovery pre-pass was guarded to TBL_MethodRef (cross-assembly
interface calls). When the interface and the caller live in the SAME assembly (e.g.
Repeater calling IDataTemplate.Build, both in nanoFramework.Lvgl.Ui) the call is a TBL_MethodDef,
so the pre-pass never ran and genericType stayed null.

Two changes in CEE_CALL/CEE_CALLVIRT (Interpreter.cpp):
- Pre-pass: prefer the receiver's own stored closed TypeSpec (obj->ObjectGenericType()) over the
  caller-assembly TypeSpec-table scan (more robust, cross-assembly correct).
- else branch (genericType still null after InitializeFromIndex): recover the closed TypeSpec from
  the receiver (IsAGenericInstance + ObjectGenericType, matched to the callee's declaring type) and
  reinitialize, mirroring the recovery already done in the genericType branch. This covers the
  same-assembly MethodDef interface dispatch the pre-pass misses.

On device (ESP32-S3): a generic DataTemplate<T> provider shim (IDataTemplate.Build(object) -> (T)item)
driving a Repeater over ObservableCollection<T> now builds, rebuilds on collection change, and
applies per-item compiled bindings with no InvalidCastException.
@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: 061db931-2981-4394-ae87-f4d334ed6ec4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@josesimoes josesimoes 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.

Apart from this, please review the comments added. They are VERY verbose. I understand AI is eager to add them, but it doesn't make sense otherwise we'll end up with more comments than code making it unreadable. Keep only the absolutely necessary ones and make them as concise as possible.


fieldCursor = reinterpret_cast<CLR_RT_HeapBlock *>(giHeader) + totFields;

CLR_Debug::Printf(

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.

These debug snippets are to be kept.

@josesimoes

Copy link
Copy Markdown
Member

@danielmeza please instruct your agent to respect the PR template. Do not remove the checklist items not checked. 😉

@josesimoes

Copy link
Copy Markdown
Member

@danielmeza we need a unit test (or more) to cover this. Please add it to mscorlib solution (develop branch). Maybe in the Types project...
The PR adding those will fail (expected) and then this will fix it.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants