Repository: dotnet/runtime
Pull request #113500: Disallow loading non-Explicit types with explicit field offsets
Author: login: jkoritzinsky
Labels: name: breaking-change
description: Issue or PR that represents a breaking API or functional change over a previous release.
name: area-VM-coreclr
Created: 2025-03-13T22:58:12Z | Merged: 2025-03-27T20:51:34Z

Today, explicit field offsets on Auto-layout types are ignored, as are offsets on Sequential types that don't qualify for ManagedSequential (and as a result end up with Auto layout).

For Sequential layout, CoreCLR allows offsets to be specified. These "offsets" specify the sequencing order of the fields in sequential layout. This behavior is disallowed (you can't specify FieldOffset on a Sequential type) and unused by Roslyn and F# and C++/CLI. It's also illegal per the ECMA-335 spec (it specifically only allows FieldOffset rows for fields in an explicit layout type).

We had one IL regression test that validated that specifying offsets for a sequential type was ignored (so not testing the above feature).

Remove this unused feature and change the type loader to throw if a non-explicit layout type has offset information for its fields.


Diff:
diff --git a/src/coreclr/tools/Common/TypeSystem/Common/CastingHelper.TypeEquivalence.cs b/src/coreclr/tools/Common/TypeSystem/Common/CastingHelper.TypeEquivalence.cs
index 8797efdde09bfd..a757df953fea38 100644
--- a/src/coreclr/tools/Common/TypeSystem/Common/CastingHelper.TypeEquivalence.cs
+++ b/src/coreclr/tools/Common/TypeSystem/Common/CastingHelper.TypeEquivalence.cs
@@ -202,6 +202,15 @@ static bool CompareStructuresForEquivalence(MetadataType type1, MetadataType typ
                     return false;
                 }
 
+                bool explicitLayout = false;
+                if (!enumMode)
+                {
+                    if (!CompareTypeLayout(type1, type2, out explicitLayout))
+                    {
+                        return false;
+                    }
+                }
+
                 // Compare field types for equivalence
                 var fields1 = type1.GetFields().GetEnumerator();
                 var fields2 = type2.GetFields().GetEnumerator();
@@ -249,20 +258,22 @@ static bool CompareStructuresForEquivalence(MetadataType type1, MetadataType typ
                     {
                         return false;
                     }
-                }
 
-                // At this point we know that the set of fields is the same, and have the same types
-                if (!enumMode)
-                {
-                    if (!CompareTypeLayout(type1, type2))
+                    // If we are in explicit layout mode, we need to compare the offsets
+                    if (explicitLayout)
                     {
-                        return false;
+                        if (field1.MetadataOffset != field2.MetadataOffset)
+                        {
+                            return false;
+                        }
                     }
                 }
+
                 return true;
 
-                static bool CompareTypeLayout(MetadataType type1, MetadataType type2)
+                static bool CompareTypeLayout(MetadataType type1, MetadataType type2, out bool explicitLayout)
                 {
+                    explicitLayout = false;
                     // Types must either be Sequential or Explicit layout
                     if (type1.IsSequentialLayout != type2.IsSequentialLayout)
                     {
@@ -279,7 +290,7 @@ static bool CompareTypeLayout(MetadataType type1, MetadataType type2)
                         return false;
                     }
 
-                    bool explicitLayout = type1.IsExplicitLayout;
+                    explicitLayout = type1.IsExplicitLayout;
 
                     // they must have the same charset
                     if (type1.PInvokeStringFormat != type2.PInvokeStringFormat)
@@ -293,21 +304,6 @@ static bool CompareTypeLayout(MetadataType type1, MetadataType type2)
                         (layoutMetadata1.Size != layoutMetadata2.Size))
                         return false;
 
-                    if ((explicitLayout) && !(layoutMetadata1.Offsets == null && layoutMetadata2.Offsets == null))
-                    {
-                        if (layoutMetadata1.Offsets == null)
-                            return false;
-
-                        if (layoutMetadata2.Offsets == null)
-                            return false;
-
-                        for (int index = 0; index < layoutMetadata1.Offsets.Length; index++)
-                        {
-                            if (layoutMetadata1.Offsets[index].Offset != layoutMetadata2.Offsets[index].Offset)
-                                return false;
-                        }
-                    }
-
                     return true;
                 }
 
diff --git a/src/coreclr/tools/Common/TypeSystem/Common/FieldDesc.FieldLayout.cs b/src/coreclr/tools/Common/TypeSystem/Common/FieldDesc.FieldLayout.cs
index e7b5e6d21478dc..b08cb305869678 100644
--- a/src/coreclr/tools/Common/TypeSystem/Common/FieldDesc.FieldLayout.cs
+++ b/src/coreclr/tools/Common/TypeSystem/Common/FieldDesc.FieldLayout.cs
@@ -47,5 +47,13 @@ internal void InitializeOffset(LayoutInt offset)
             Debug.Assert(_offset == FieldAndOffset.InvalidOffset || _offset == offset);
             _offset = offset;
         }
+
+        public virtual LayoutInt MetadataOffset
+        {
+            get
+            {
+                return LayoutInt.Indeterminate;
+            }
+        }
     }
 }
diff --git a/src/coreclr/tools/Common/TypeSystem/Common/FieldForInstantiatedType.FieldLayout.cs b/src/coreclr/tools/Common/TypeSystem/Common/FieldForInstantiatedType.FieldLayout.cs
new file mode 100644
index 00000000000000..b355d52d3a72f9
--- /dev/null
+++ b/src/coreclr/tools/Common/TypeSystem/Common/FieldForInstantiatedType.FieldLayout.cs
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Debug = System.Diagnostics.Debug;
+
+namespace Internal.TypeSystem
+{
+    public sealed partial class FieldForInstantiatedType : FieldDesc
+    {
+        public override LayoutInt MetadataOffset
+        {
+            get
+            {
+                return _fieldDef.MetadataOffset;
+            }
+        }
+    }
+}
diff --git a/src/coreclr/tools/Common/TypeSystem/Common/FieldLayoutAlgorithm.cs b/src/coreclr/tools/Common/TypeSystem/Common/FieldLayoutAlgorithm.cs
index 09818b42cf7de2..38967539503687 100644
--- a/src/coreclr/tools/Common/TypeSystem/Common/FieldLayoutAlgorithm.cs
+++ b/src/coreclr/tools/Common/TypeSystem/Common/FieldLayoutAlgorithm.cs
@@ -80,6 +80,21 @@ public enum StaticLayoutKind
         StaticRegionSizesAndFields
     }
 
+    public struct FieldAndOffset
+    {
+        public static readonly LayoutInt InvalidOffset = new LayoutInt(int.MaxValue);
+
+        public readonly FieldDesc Field;
+
+        public readonly LayoutInt Offset;
+
+        public FieldAndOffset(FieldDesc field, LayoutInt offset)
+        {
+            Field = field;
+            Offset = offset;
+        }
+    }
+
     public struct ComputedInstanceFieldLayout
     {
         public LayoutInt FieldSize;
diff --git a/src/coreclr/tools/Common/TypeSystem/Common/MetadataFieldLayoutAlgorithm.cs b/src/coreclr/tools/Common/TypeSystem/Common/MetadataFieldLayoutAlgorithm.cs
index 7a1a05ad26488b..1bbb7b3dab4072 100644
--- a/src/coreclr/tools/Common/TypeSystem/Common/MetadataFieldLayoutAlgorithm.cs
+++ b/src/coreclr/tools/Common/TypeSystem/Common/MetadataFieldLayoutAlgorithm.cs
@@ -144,15 +144,12 @@ out instanceByteSizeAndAlignment
                 }
 
                 var layoutMetadata = type.GetClassLayout();
-
                 // If packing is out of range or not a power of two, throw that the size is invalid
                 int packing = layoutMetadata.PackingSize;
                 if (packing < 0 || packing > 128 || ((packing & (packing - 1)) != 0))
                 {
                     ThrowHelper.ThrowTypeLoadException(ExceptionStringID.ClassLoadBadFormat, type);
                 }
-
-                Debug.Assert(layoutMetadata.Offsets == null || layoutMetadata.Offsets.Length == numInstanceFields);
             }
 
             // At this point all special cases are handled and all inputs validated
@@ -329,9 +326,12 @@ protected ComputedInstanceFieldLayout ComputeExplicitFieldLayout(MetadataType ty
                 hasVectorTField = type.BaseType.IsVectorTOrHasVectorTFields;
             }
 
-            foreach (FieldAndOffset fieldAndOffset in layoutMetadata.Offsets)
+            foreach (FieldDesc field in type.GetFields())
             {
-                TypeDesc fieldType = fieldAndOffset.Field.FieldType;
+                if (field.IsStatic)
+                    continue;
+
+                TypeDesc fieldType = field.FieldType;
                 var fieldSizeAndAlignment = ComputeFieldSizeAndAlignment(fieldType.UnderlyingType, hasLayout: true, packingSize, out ComputedFieldData fieldData);
                 if (!fieldData.LayoutAbiStable)
                     layoutAbiStable = false;
@@ -344,10 +344,12 @@ protected ComputedInstanceFieldLayout ComputeExplicitFieldLayout(MetadataType ty
 
                 largestAlignmentRequired = LayoutInt.Max(fieldSizeAndAlignment.Alignment, largestAlignmentRequired);
 
-                if (fieldAndOffset.Offset == FieldAndOffset.InvalidOffset)
+                LayoutInt metadataOffset = field.MetadataOffset;
+
+                if (metadataOffset == LayoutInt.Indeterminate)
                     ThrowHelper.ThrowTypeLoadException(ExceptionStringID.ClassLoadBadFormat, type);
 
-                LayoutInt computedOffset = fieldAndOffset.Offset + cumulativeInstanceFieldPos + offsetBias;
+                LayoutInt computedOffset = metadataOffset + cumulativeInstanceFieldPos + offsetBias;
 
                 // GC pointers MUST be aligned.
                 bool needsToBeAligned =
@@ -363,11 +365,11 @@ protected ComputedInstanceFieldLayout ComputeExplicitFieldLayout(MetadataType ty
                     int offsetModulo = computedOffset.AsInt % type.Context.Target.PointerSize;
                     if (offsetModulo != 0)
                     {
-                        ThrowHelper.ThrowTypeLoadException(ExceptionStringID.ClassLoadExplicitLayout, type, fieldAndOffset.Offset.ToStringInvariant());
+                        ThrowHelper.ThrowTypeLoadException(ExceptionStringID.ClassLoadExplicitLayout, type, metadataOffset.ToStringInvariant());
                     }
                 }
 
-                offsets[fieldOrdinal] = new FieldAndOffset(fieldAndOffset.Field, computedOffset);
+                offsets[fieldOrdinal] = new FieldAndOffset(field, computedOffset);
 
                 LayoutInt fieldExtent = computedOffset + fieldSizeAndAlignment.Size;
                 instanceSize = LayoutInt.Max(fieldExtent, instanceSize);
diff --git a/src/coreclr/tools/Common/TypeSystem/Common/MetadataType.cs b/src/coreclr/tools/Common/TypeSystem/Common/MetadataType.cs
index 722aa66a878f01..18d3ee8f754d59 100644
--- a/src/coreclr/tools/Common/TypeSystem/Common/MetadataType.cs
+++ b/src/coreclr/tools/Common/TypeSystem/Common/MetadataType.cs
@@ -110,21 +110,5 @@ public struct ClassLayoutMetadata
     {
         public int PackingSize;
         public int Size;
-        public FieldAndOffset[] Offsets;
-    }
-
-    public struct FieldAndOffset
-    {
-        public static readonly LayoutInt InvalidOffset = new LayoutInt(int.MaxValue);
-
-        public readonly FieldDesc Field;
-
-        public readonly LayoutInt Offset;
-
-        public FieldAndOffset(FieldDesc field, LayoutInt offset)
-        {
-            Field = field;
-            Offset = offset;
-        }
     }
 }
diff --git a/src/coreclr/tools/Common/TypeSystem/Ecma/EcmaField.cs b/src/coreclr/tools/Common/TypeSystem/Ecma/EcmaField.cs
index 01356a7200052d..8812678f72ae17 100644
--- a/src/coreclr/tools/Common/TypeSystem/Ecma/EcmaField.cs
+++ b/src/coreclr/tools/Common/TypeSystem/Ecma/EcmaField.cs
@@ -297,6 +297,15 @@ public override MarshalAsDescriptor GetMarshalAsDescriptor()
 
             return null;
         }
+
+        public override LayoutInt MetadataOffset
+        {
+            get
+            {
+                int offset = MetadataReader.GetFieldDefinition(_handle).GetOffset();
+                return offset == -1 ? LayoutInt.Indeterminate : new LayoutInt(offset);
+            }
+        }
     }
 
     public static class EcmaFieldExtensions
diff --git a/src/coreclr/tools/Common/TypeSystem/Ecma/EcmaType.cs b/src/coreclr/tools/Common/TypeSystem/Ecma/EcmaType.cs
index 3c349b3615c3d2..a444ebb2e3b596 100644
--- a/src/coreclr/tools/Common/TypeSystem/Ecma/EcmaType.cs
+++ b/src/coreclr/tools/Common/TypeSystem/Ecma/EcmaType.cs
@@ -567,46 +567,11 @@ public override ClassLayoutMetadata GetClassLayout()
         {
             TypeLayout layout = _typeDefinition.GetLayout();
 
-            ClassLayoutMetadata result;
-            result.PackingSize = layout.PackingSize;
-            result.Size = layout.Size;
-
-            // Skip reading field offsets if this is not explicit layout
-            if (IsExplicitLayout)
+            return new ClassLayoutMetadata
             {
-                var fieldDefinitionHandles = _typeDefinition.GetFields();
-                var numInstanceFields = 0;
-
-                foreach (var handle in fieldDefinitionHandles)
-                {
-                    var fieldDefinition = MetadataReader.GetFieldDefinition(handle);
-                    if ((fieldDefinition.Attributes & FieldAttributes.Static) != 0)
-                        continue;
-
-                    numInstanceFields++;
-                }
-
-                result.Offsets = new FieldAndOffset[numInstanceFields];
-
-                int index = 0;
-                foreach (var handle in fieldDefinitionHandles)
-                {
-                    var fieldDefinition = MetadataReader.GetFieldDefinition(handle);
-                    if ((fieldDefinition.Attributes & FieldAttributes.Static) != 0)
-                        continue;
-
-                    // Note: GetOffset() returns -1 when offset was not set in the metadata
-                    int specifiedOffset = fieldDefinition.GetOffset();
-                    result.Offsets[index] =
-                        new FieldAndOffset(_module.GetField(handle, this), specifiedOffset == -1 ? FieldAndOffset.InvalidOffset : new LayoutInt(specifiedOffset));
-
-                    index++;
-                }
-            }
-            else
-                result.Offsets = null;
-
-            return result;
+                PackingSize = layout.PackingSize,
+                Size = layout.Size
+            };
         }
 
         public override bool IsExplicitLayout
diff --git a/src/coreclr/tools/Common/TypeSystem/Interop/IL/NativeStructType.cs b/src/coreclr/tools/Common/TypeSystem/Interop/IL/NativeStructType.cs
index 59561328990062..3e0c35c0ae9210 100644
--- a/src/coreclr/tools/Common/TypeSystem/Interop/IL/NativeStructType.cs
+++ b/src/coreclr/tools/Common/TypeSystem/Interop/IL/NativeStructType.cs
@@ -249,29 +249,6 @@ public override ClassLayoutMetadata GetClassLayout()
             result.PackingSize = layout.PackingSize;
             result.Size = layout.Size;
 
-            if (IsExplicitLayout)
-            {
-                result.Offsets = new FieldAndOffset[layout.Offsets.Length];
-
-                Debug.Assert(layout.Offsets.Length <= _fields.Length);
-
-                int layoutIndex = 0;
-                for (int index = 0; index < _fields.Length; index++)
-                {
-                    if (_fields[index].Name == layout.Offsets[layoutIndex].Field.Name)
-                    {
-                        result.Offsets[layoutIndex] = new FieldAndOffset(_fields[index], layout.Offsets[layoutIndex].Offset);
-                        layoutIndex++;
-                    }
-                }
-
-                Debug.Assert(layoutIndex == layout.Offsets.Length);
-            }
-            else
-            {
-                result.Offsets = null;
-            }
-
             return result;
         }
 
@@ -440,6 +417,14 @@ public override string Name
                 }
             }
 
+            public override LayoutInt MetadataOffset
+            {
+                get
+                {
+                    return _managedField.MetadataOffset;
+                }
+            }
+
             public NativeStructField(TypeDesc nativeType, MetadataType owningType, FieldDesc managedField)
             {
                 _fieldType = nativeType;
diff --git a/src/coreclr/tools/ILVerification/ILVerification.projitems b/src/coreclr/tools/ILVerification/ILVerification.projitems
index fc02753d601044..764cad13363b4e 100644
--- a/src/coreclr/tools/ILVerification/ILVerification.projitems
+++ b/src/coreclr/tools/ILVerification/ILVerification.projitems
@@ -99,6 +99,9 @@
     <Compile Include="$(ToolsCommonPath)TypeSystem\Common\FieldForInstantiatedType.cs">
       <Link>TypeSystem\Common\FieldForInstantiatedType.cs</Link>
     </Compile>
+    <Compile Include="$(ToolsCommonPath)TypeSystem\Common\FieldForInstantiatedType.FieldLayout.cs">
+      <Link>TypeSystem\Common\FieldForInstantiatedType.FieldLayout.cs</Link>
+    </Compile>
     <Compile Include="$(ToolsCommonPath)TypeSystem\Common\FieldDesc.cs">
       <Link>TypeSystem\Common\FieldDesc.cs</Link>
     </Compile>
diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/CompilerTypeSystemContext.GeneratedAssembly.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/CompilerTypeSystemContext.GeneratedAssembly.cs
index fc229985862639..ba9801349625c4 100644
--- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/CompilerTypeSystemContext.GeneratedAssembly.cs
+++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/CompilerTypeSystemContext.GeneratedAssembly.cs
@@ -153,7 +153,6 @@ public override ClassLayoutMetadata GetClassLayout()
             {
                 return new ClassLayoutMetadata
                 {
-                    Offsets = null,
                     PackingSize = 0,
                     Size = 0,
                 };
diff --git a/src/coreclr/tools/aot/ILCompiler.TypeSystem/ILCompiler.TypeSystem.csproj b/src/coreclr/tools/aot/ILCompiler.TypeSystem/ILCompiler.TypeSystem.csproj
index cafa4952376f5c..6bdea9c03cbe98 100644
--- a/src/coreclr/tools/aot/ILCompiler.TypeSystem/ILCompiler.TypeSystem.csproj
+++ b/src/coreclr/tools/aot/ILCompiler.TypeSystem/ILCompiler.TypeSystem.csproj
@@ -237,6 +237,9 @@
     <Compile Include="..\..\Common\TypeSystem\Common\FieldForInstantiatedType.cs">
       <Link>TypeSystem\Common\FieldForInstantiatedType.cs</Link>
     </Compile>
+    <Compile Include="..\..\Common\TypeSystem\Common\FieldForInstantiatedType.FieldLayout.cs">
+      <Link>TypeSystem\Common\FieldForInstantiatedType.FieldLayout.cs</Link>
+    </Compile>
     <Compile Include="..\..\Common\TypeSystem\Common\FieldDesc.cs">
       <Link>TypeSystem\Common\FieldDesc.cs</Link>
     </Compile>
diff --git a/src/coreclr/vm/classlayoutinfo.cpp b/src/coreclr/vm/classlayoutinfo.cpp
index b7290c5a5c3b5f..ef3b34a212b157 100644
--- a/src/coreclr/vm/classlayoutinfo.cpp
+++ b/src/coreclr/vm/classlayoutinfo.cpp
@@ -44,12 +44,7 @@ namespace
             // if we haven't found a matching token, it must be a static field with layout -- ignore it
             if (pfwalk->m_MD != fd) continue;
 
-            if (!fExplicitOffsets)
-            {
-                // ulOffset is the sequence
-                pfwalk->m_sequence = ulOffset;
-            }
-            else
+            if (fExplicitOffsets)
             {
                 // ulOffset is the explicit offset
                 pfwalk->m_placement.m_offset = ulOffset;


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR removes support for explicit field offsets on non-explicit layout types by making the type loader throw a TypeLoadException when such offsets are detected.  
- Added tests in AutoLayout and SequentialOffsets to validate that a TypeLoadException is thrown.  
- Ensured both auto and sequential layout tests check for the disallowed feature.

### Reviewed Changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

| File | Description |
| ---- | ----------- |
| src/tests/Loader/classloader/AutoLayout/AutoLayout.cs | Adds a test to confirm that Auto-layout types with field offsets throw a TypeLoadException. |
| src/tests/Loader/classloader/SequentialLayout/Offsets/SequentialOffsets.cs | Adds a similar test for Sequential layout types with field offsets. |




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T22:59:05Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request introduces tests to verify that attempting to load non-explicit layout types with explicit field offsets now throws a TypeLoadException, enforcing the ECMA-335 specification.  
- Added test in AutoLayout to ensure explicit field offsets are disallowed for auto-layout types.  
- Added test in SequentialOffsets to ensure explicit field offsets are disallowed for sequential layout types.

### Reviewed Changes

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

| File | Description |
| ---- | ----------- |
| src/tests/Loader/classloader/AutoLayout/AutoLayout.cs | Adds a test for Auto-layout types with explicit field offsets |
| src/tests/Loader/classloader/SequentialLayout/Offsets/SequentialOffsets.cs | Adds a test for Sequential layout types with explicit field offsets |




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T23:08:39Z
user: login: MichalStrehovsky
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T22:38:11Z
user: login: MichalStrehovsky
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T22:39:48Z
user: login: jkoritzinsky
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T23:02:34Z
user: login: jkotas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T05:35:58Z
user: login: MichalStrehovsky
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T07:42:06Z
user: login: MichalStrehovsky
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T07:42:35Z
user: login: jkotas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T00:02:39Z
user: login: jkotas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T00:25:26Z
user: login: jkoritzinsky
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T17:06:18Z
user: login: jkoritzinsky
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T20:45:21Z
user: login: jkotas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T20:48:59Z
user: login: jkoritzinsky
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T21:57:16Z
user: login: jkotas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T00:08:18Z
user: login: jkoritzinsky
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T00:22:28Z
user: login: jkoritzinsky
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T17:56:49Z
user: login: jkotas
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-27T18:04:08Z

Review comments:
diff_hunk: @@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Runtime.CompilerServices;
+using Xunit;
+
+public static class AutoLayout
+{
+    [Fact]
+    public static void FieldsWithOffsets()
+    {
+        Assert.Throws<TypeLoadException>([MethodImpl(MethodImplOptions.NoInlining)] () => typeof(AutoLayoutTypeWithFieldsWithOffsets));
path: src/tests/Loader/classloader/AutoLayout/AutoLayout.cs
user: login: Copilot
body: Applying [MethodImpl(MethodImplOptions.NoInlining)] directly on a lambda expression may not be supported and could fail to prevent inlining as intended. Consider refactoring the lambda into a local function decorated with the attribute.
```suggestion
        Assert.Throws<TypeLoadException>(NoInlineMethod);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static Type NoInlineMethod()
    {
        return typeof(AutoLayoutTypeWithFieldsWithOffsets);
```
created_at: 2025-03-13T22:59:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Runtime.CompilerServices;
+using Xunit;
+
+public static class SequentialOffsets
+{
+    [Fact]
+    public static void FieldsWithOffsets()
+    {
+        Assert.Throws<TypeLoadException>([MethodImpl(MethodImplOptions.NoInlining)] () => typeof(SeqLayoutTypeWithFieldsWithOffsets));
path: src/tests/Loader/classloader/SequentialLayout/Offsets/SequentialOffsets.cs
user: login: Copilot
body: Applying [MethodImpl(MethodImplOptions.NoInlining)] directly on a lambda expression may not be supported and could fail to prevent inlining as intended. Consider refactoring the lambda into a local function decorated with the attribute.
```suggestion
        Assert.Throws<TypeLoadException>(NoInlineMethod);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static void NoInlineMethod()
    {
        _ = typeof(SeqLayoutTypeWithFieldsWithOffsets);
```
created_at: 2025-03-13T22:59:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -134,6 +134,7 @@ out instanceByteSizeAndAlignment
                 return result;
             }
 
+            var layoutMetadata = type.GetClassLayout();
path: src/coreclr/tools/Common/TypeSystem/Common/MetadataFieldLayoutAlgorithm.cs
user: login: MichalStrehovsky
body: This is very subtle. Maybe now that we need to look at field offsets from metadata even for non explicit layout types, `layoutMetadata` containing the `FieldAndOffset[]` outlived it's usefulness - maybe we should drop that field and add a first class `virtual LayoutInt MetadataOffset` property in `FieldDesc.Layout.cs`. Then we can check it in each layouting strategy as we're doing layout.
created_at: 2025-03-18T22:38:11Z
author_association: MEMBER
diff_hunk: @@ -134,6 +134,7 @@ out instanceByteSizeAndAlignment
                 return result;
             }
 
+            var layoutMetadata = type.GetClassLayout();
path: src/coreclr/tools/Common/TypeSystem/Common/MetadataFieldLayoutAlgorithm.cs
user: login: MichalStrehovsky
body: (I can sketch that out in a .diff or push it to the branch tomorrow if it helps.)
created_at: 2025-03-18T22:39:48Z
author_association: MEMBER
diff_hunk: @@ -134,6 +134,7 @@ out instanceByteSizeAndAlignment
                 return result;
             }
 
+            var layoutMetadata = type.GetClassLayout();
path: src/coreclr/tools/Common/TypeSystem/Common/MetadataFieldLayoutAlgorithm.cs
user: login: jkoritzinsky
body: I'll take a shot at it before the end of the day and we can see what we think of it.
created_at: 2025-03-18T23:02:34Z
author_association: MEMBER
diff_hunk: @@ -394,7 +394,8 @@ BEGIN
     IDS_INVALID_REDIM                       "Illegal attempt to replace or redimension a fixed or locked SafeArray."
 
     IDS_INVALID_PINVOKE_CALLCONV            "Unsupported unmanaged calling convention."
-    IDS_CLASSLOAD_NSTRUCT_EXPLICIT_OFFSET   "Could not load type '%1' from assembly '%2' because field '%3' was not given an explicit offset."
+    IDS_CLASSLOAD_EXPSTRUCT_EXPLICIT_OFFSET "Could not load type '%1' from assembly '%2' because field '%3' was not given an explicit offset."
+    IDS_CLASSLOAD_STRUCT_EXPLICIT_OFFSET    "Could not load type '%1' from assembly '%2' because field '%3' in a non-explicit type was given an explicit offset."
path: src/coreclr/dlls/mscorrc/mscorrc.rc
user: login: jkotas
body: ```suggestion
    IDS_CLASSLOAD_STRUCT_EXPLICIT_OFFSET    "Could not load type '%1' from assembly '%2' because field '%3' in was given an explicit offset, that is only allowed in types with explicit layout."
```
"non-explicit type" is not an established term
created_at: 2025-03-19T05:35:57Z
author_association: MEMBER
diff_hunk: @@ -293,17 +293,37 @@ static bool CompareTypeLayout(MetadataType type1, MetadataType type2)
                         (layoutMetadata1.Size != layoutMetadata2.Size))
                         return false;
 
-                    if ((explicitLayout) && !(layoutMetadata1.Offsets == null && layoutMetadata2.Offsets == null))
+                    if (explicitLayout)
path: src/coreclr/tools/Common/TypeSystem/Common/CastingHelper.TypeEquivalence.cs
user: login: MichalStrehovsky
body: We walk all fields on both types starting at line 209 in this file already - I would just add the check there so that we don't need to do this ugly compare-two-IEnumerables dance twice.
created_at: 2025-03-19T07:42:06Z
author_association: MEMBER
diff_hunk: @@ -134,6 +134,7 @@ out instanceByteSizeAndAlignment
                 return result;
             }
 
+            var layoutMetadata = type.GetClassLayout();
path: src/coreclr/tools/Common/TypeSystem/Common/MetadataFieldLayoutAlgorithm.cs
user: login: MichalStrehovsky
body: > I'll take a shot at it before the end of the day and we can see what we think of it.

This turned out pretty nice, thank you!
created_at: 2025-03-19T07:42:35Z
author_association: MEMBER
diff_hunk: @@ -110,7 +110,6 @@ public struct ClassLayoutMetadata
     {
         public int PackingSize;
         public int Size;
-        public FieldAndOffset[] Offsets;
     }
 
     public struct FieldAndOffset
path: src/coreclr/tools/Common/TypeSystem/Common/MetadataType.cs
user: login: jkotas
body: This should be moved to `FieldLayoutAlgorithm.cs` or `FieldDesc.FieldLayout.cs` since it is no longer used in the common MetadataType.
created_at: 2025-03-23T00:02:38Z
author_association: MEMBER
diff_hunk: @@ -4299,6 +4299,25 @@ VOID    MethodTableBuilder::InitializeFieldDescs(FieldDesc *pFieldDescList,
                 pwalk++;
             }
         }
+        else
+        {
+            ULONG fieldOffset = 0;
+            if (pInternalImport->GetFieldOffset(bmtMetaData->pFields[i], &fieldOffset) == S_OK)
path: src/coreclr/vm/methodtablebuilder.cpp
user: login: jkotas
body: This is an extra binary search in metadata tables for every field. Can we do the binary search just once per type?

I am wondering whether we need to check this at all. I think silently ignoring the explicit offsets for auto-layout classes like what are doing today is fine. We do not generally check for silently ignored attributes.
created_at: 2025-03-23T00:25:26Z
author_association: MEMBER
diff_hunk: @@ -4299,6 +4299,25 @@ VOID    MethodTableBuilder::InitializeFieldDescs(FieldDesc *pFieldDescList,
                 pwalk++;
             }
         }
+        else
+        {
+            ULONG fieldOffset = 0;
+            if (pInternalImport->GetFieldOffset(bmtMetaData->pFields[i], &fieldOffset) == S_OK)
path: src/coreclr/vm/methodtablebuilder.cpp
user: login: jkoritzinsky
body: I'm fine removing this from auto-layout and changing the breaking change to sequential only.

I wanted to start with a wider net and narrow it down if requested.
created_at: 2025-03-24T17:06:17Z
author_association: MEMBER
diff_hunk: @@ -4299,6 +4299,25 @@ VOID    MethodTableBuilder::InitializeFieldDescs(FieldDesc *pFieldDescList,
                 pwalk++;
             }
         }
+        else
+        {
+            ULONG fieldOffset = 0;
+            if (pInternalImport->GetFieldOffset(bmtMetaData->pFields[i], &fieldOffset) == S_OK)
path: src/coreclr/vm/methodtablebuilder.cpp
user: login: jkoritzinsky
body: I think we can do the search once per type, just not with the APIs on `IMDInternalImport`. I'll see if I can do that cleanly.
created_at: 2025-03-26T20:45:21Z
author_association: MEMBER
diff_hunk: @@ -4299,6 +4299,25 @@ VOID    MethodTableBuilder::InitializeFieldDescs(FieldDesc *pFieldDescList,
                 pwalk++;
             }
         }
+        else
+        {
+            ULONG fieldOffset = 0;
+            if (pInternalImport->GetFieldOffset(bmtMetaData->pFields[i], &fieldOffset) == S_OK)
path: src/coreclr/vm/methodtablebuilder.cpp
user: login: jkotas
body: What do you think about not doing this at all and just silently ignoring the explicit offsets for types without explicit layout?
created_at: 2025-03-26T20:48:58Z
author_association: MEMBER
diff_hunk: @@ -4299,6 +4299,25 @@ VOID    MethodTableBuilder::InitializeFieldDescs(FieldDesc *pFieldDescList,
                 pwalk++;
             }
         }
+        else
+        {
+            ULONG fieldOffset = 0;
+            if (pInternalImport->GetFieldOffset(bmtMetaData->pFields[i], &fieldOffset) == S_OK)
path: src/coreclr/vm/methodtablebuilder.cpp
user: login: jkoritzinsky
body: It looks like writing a more efficient implementation here is remarkably difficult with our current metadata API. We'd have to write a new implementation and we need a primitive of "binary search for the closest entry", which the current implementation doesn't have.

We can't even use `GetClassLayoutInit` and `GetClassLayoutNext` as that does the binary search each time.

I'd happily change this to just silently ignore the offsets for non-explicit types completely. @davidwrighton suggested adding the error for sequential types as we're removing a (compeletely unused) feature. I wanted to expand it to validate for all types, but I think that's impractical at this point.

Let me know if you want this to be done for sequential-only or not at all.
created_at: 2025-03-26T21:57:16Z
author_association: MEMBER
diff_hunk: @@ -4299,6 +4299,25 @@ VOID    MethodTableBuilder::InitializeFieldDescs(FieldDesc *pFieldDescList,
                 pwalk++;
             }
         }
+        else
+        {
+            ULONG fieldOffset = 0;
+            if (pInternalImport->GetFieldOffset(bmtMetaData->pFields[i], &fieldOffset) == S_OK)
path: src/coreclr/vm/methodtablebuilder.cpp
user: login: jkotas
body: My preference is not at all.

I am really only worried about cases that can be produced by C# or other mainstream high-level compiler. I am not worried about hand-written IL.
created_at: 2025-03-27T00:08:17Z
author_association: MEMBER
diff_hunk: @@ -4299,6 +4299,25 @@ VOID    MethodTableBuilder::InitializeFieldDescs(FieldDesc *pFieldDescList,
                 pwalk++;
             }
         }
+        else
+        {
+            ULONG fieldOffset = 0;
+            if (pInternalImport->GetFieldOffset(bmtMetaData->pFields[i], &fieldOffset) == S_OK)
path: src/coreclr/vm/methodtablebuilder.cpp
user: login: jkoritzinsky
body: Sounds great to me!
created_at: 2025-03-27T00:22:28Z
author_association: MEMBER
diff_hunk: @@ -4299,6 +4299,25 @@ VOID    MethodTableBuilder::InitializeFieldDescs(FieldDesc *pFieldDescList,
                 pwalk++;
             }
         }
+        else
+        {
+            ULONG fieldOffset = 0;
+            if (pInternalImport->GetFieldOffset(bmtMetaData->pFields[i], &fieldOffset) == S_OK)
path: src/coreclr/vm/methodtablebuilder.cpp
user: login: jkoritzinsky
body: I've removed the error checking for sequential layout as well. The new behavior is to ignore offsets for all non-explicit layouts as they can only be done so with hand-written IL.
created_at: 2025-03-27T17:56:49Z
author_association: MEMBER

PR comments:
user: login: dotnet-policy-service[bot]
created_at: 2025-03-13T22:58:59Z
body: Tagging subscribers to this area: @mangod9
See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.
<!-- Policy app identification https://img.shields.io/static/v1?label=PullRequestIssueManagement. -->
author_association: CONTRIBUTOR
user: login: kg
created_at: 2025-03-19T16:52:47Z
body: LGTM other than the existing comments from others
author_association: MEMBER
user: login: jkoritzinsky
created_at: 2025-03-27T20:51:24Z
body: /ba-g timeouts look unrelated.
author_association: MEMBER
user: login: ericstj
created_at: 2025-10-03T20:48:13Z
body: ## 📋 Breaking Change Documentation Required

[Create a breaking change issue with AI-generated content](https://github.com/dotnet/docs/issues/new?title=%5BBreaking%20change%5D%3A%20Disallow%20loading%20non-Explicit%20types%20with%20explicit%20field%20offsets&body=%23%23%23%20Breaking%20Change%3A%20Disallow%20loading%20non-Explicit%20types%20with%20explicit%20field%20offsets%0A%0A%23%23%23%23%20Description%0A%0AStarting%20in%20.NET%2010%20Preview%204,%20the%20.NET%20runtime%20enforces%20stricter%20validation%20for%20type%20layouts.%20Specifically,%20the%20runtime%20now%20throws%20a%20`TypeLoadException`%20if%20a%20type%20with%20a%20non-explicit%20layout%20%28e.g.,%20`Auto`%20or%20`Sequential`%29%20specifies%20explicit%20field%20offsets%20using%20the%20`%5BFieldOffset%5D`%20attribute.%20This%20change%20aligns%20the%20runtime%27s%20behavior%20with%20the%20ECMA-335%20specification,%20which%20only%20permits%20explicit%20field%20offsets%20for%20types%20with%20an%20`Explicit`%20layout.%0A%0APreviously,%20the%20runtime%20ignored%20explicit%20field%20offsets%20on%20non-explicit%20layout%20types,%20which%20could%20lead%20to%20unexpected%20behavior%20or%20incorrect%20assumptions%20about%20memory%20layout.%0A%0AThis%20change%20was%20introduced%20in%20%5BPR%20%23113500%5D%28https%3A//github.com/dotnet/runtime/pull/113500%29.%0A%0A%23%23%23%23%20Version%0A%0AThis%20breaking%20change%20was%20introduced%20in%20**.NET%2010%20Preview%204**.%0A%0A%23%23%23%23%20Previous%20behavior%0A%0AIn%20earlier%20versions%20of%20.NET,%20the%20runtime%20ignored%20explicit%20field%20offsets%20on%20types%20with%20`Auto`%20or%20`Sequential`%20layouts.%20For%20example%3A%0A%0A```csharp%0Ausing%20System%3B%0Ausing%20System.Runtime.InteropServices%3B%0A%0A%5BStructLayout%28LayoutKind.Sequential%29%5D%0Apublic%20struct%20MyStruct%0A{%0A%20%20%20%20%5BFieldOffset%280%29%5D%20//%20Ignored%20in%20.NET%20versions%20prior%20to%2010%20Preview%204%0A%20%20%20%20public%20int%20Field1%3B%0A%0A%20%20%20%20%5BFieldOffset%284%29%5D%20//%20Ignored%20in%20.NET%20versions%20prior%20to%2010%20Preview%204%0A%20%20%20%20public%20int%20Field2%3B%0A}%0A%0Aclass%20Program%0A{%0A%20%20%20%20static%20void%20Main%28%29%0A%20%20%20%20{%0A%20%20%20%20%20%20%20%20Console.WriteLine%28%22Struct%20loaded%20successfully.%22%29%3B%0A%20%20%20%20}%0A}%0A```%0A%0AThe%20above%20code%20would%20execute%20without%20error,%20and%20the%20`%5BFieldOffset%5D`%20attributes%20would%20be%20ignored.%0A%0A%23%23%23%23%20New%20behavior%0A%0AStarting%20in%20.NET%2010%20Preview%204,%20the%20runtime%20enforces%20the%20ECMA-335%20specification%20and%20throws%20a%20`TypeLoadException`%20if%20a%20type%20with%20`Auto`%20or%20`Sequential`%20layout%20specifies%20explicit%20field%20offsets.%20The%20following%20code%20will%20now%20throw%20an%20exception%3A%0A%0A```csharp%0Ausing%20System%3B%0Ausing%20System.Runtime.InteropServices%3B%0A%0A%5BStructLayout%28LayoutKind.Sequential%29%5D%0Apublic%20struct%20MyStruct%0A{%0A%20%20%20%20%5BFieldOffset%280%29%5D%20//%20Causes%20a%20TypeLoadException%20in%20.NET%2010%20Preview%204%20and%20later%0A%20%20%20%20public%20int%20Field1%3B%0A%0A%20%20%20%20%5BFieldOffset%284%29%5D%20//%20Causes%20a%20TypeLoadException%20in%20.NET%2010%20Preview%204%20and%20later%0A%20%20%20%20public%20int%20Field2%3B%0A}%0A%0Aclass%20Program%0A{%0A%20%20%20%20static%20void%20Main%28%29%0A%20%20%20%20{%0A%20%20%20%20%20%20%20%20Console.WriteLine%28%22Struct%20loaded%20successfully.%22%29%3B%0A%20%20%20%20}%0A}%0A```%0A%0A**Exception%20thrown%3A**%0A```%0ASystem.TypeLoadException%3A%20Explicit%20field%20offsets%20are%20not%20allowed%20on%20types%20with%20Sequential%20or%20Auto%20layout.%0A```%0A%0A%23%23%23%23%20Type%20of%20breaking%20change%0A%0A-%20**Binary%20incompatible**%3A%20Existing%20binaries%20that%20define%20non-explicit%20layout%20types%20with%20explicit%20field%20offsets%20will%20fail%20to%20load.%0A-%20**Behavioral%20change**%3A%20Existing%20binaries%20might%20behave%20differently%20at%20runtime%20due%20to%20the%20stricter%20validation.%0A%0A%23%23%23%23%20Reason%20for%20change%0A%0AThis%20change%20was%20introduced%20to%20align%20the%20.NET%20runtime%20with%20the%20ECMA-335%20specification,%20which%20explicitly%20disallows%20explicit%20field%20offsets%20on%20types%20with%20`Auto`%20or%20`Sequential`%20layouts.%20The%20previous%20behavior%20of%20ignoring%20these%20offsets%20was%20non-compliant%20and%20could%20lead%20to%20unexpected%20behavior%20or%20incorrect%20assumptions%20about%20memory%20layout.%0A%0A%23%23%23%23%20Recommended%20action%0A%0ATo%20resolve%20this%20issue,%20developers%20should%20update%20their%20code%20to%20use%20`LayoutKind.Explicit`%20for%20types%20that%20specify%20explicit%20field%20offsets.%20For%20example%3A%0A%0A```csharp%0Ausing%20System%3B%0Ausing%20System.Runtime.InteropServices%3B%0A%0A%5BStructLayout%28LayoutKind.Explicit%29%5D%20//%20Use%20Explicit%20layout%0Apublic%20struct%20MyStruct%0A{%0A%20%20%20%20%5BFieldOffset%280%29%5D%0A%20%20%20%20public%20int%20Field1%3B%0A%0A%20%20%20%20%5BFieldOffset%284%29%5D%0A%20%20%20%20public%20int%20Field2%3B%0A}%0A%0Aclass%20Program%0A{%0A%20%20%20%20static%20void%20Main%28%29%0A%20%20%20%20{%0A%20%20%20%20%20%20%20%20Console.WriteLine%28%22Struct%20loaded%20successfully.%22%29%3B%0A%20%20%20%20}%0A}%0A```%0A%0AAlternatively,%20if%20explicit%20field%20offsets%20are%20not%20required,%20remove%20the%20`%5BFieldOffset%5D`%20attributes%20and%20rely%20on%20the%20default%20behavior%20of%20`Sequential`%20or%20`Auto`%20layout.%0A%0A%23%23%23%23%20Affected%20APIs%0A%0AThis%20change%20does%20not%20directly%20affect%20specific%20APIs%20but%20impacts%20the%20runtime%20behavior%20of%20types%20defined%20with%20the%20following%20attributes%3A%0A-%20`%5BStructLayout%28LayoutKind.Sequential%29%5D`%0A-%20`%5BStructLayout%28LayoutKind.Auto%29%5D`%0A-%20`%5BFieldOffset%5D`%0A%0A%23%23%23%23%20Feature%20area%0A%0A-%20VM-coreclr%0A-%20Interop%0A-%20Serialization%0A%0A%23%23%23%23%20Additional%20resources%0A%0A-%20%5BECMA-335%20Specification%5D%28https%3A//www.ecma-international.org/publications-and-standards/standards/ecma-335/%29%0A-%20%5BStructLayoutAttribute%20documentation%5D%28https%3A//learn.microsoft.com/dotnet/api/system.runtime.interopservices.structlayoutattribute%29%0A-%20%5BFieldOffsetAttribute%20documentation%5D%28https%3A//learn.microsoft.com/dotnet/api/system.runtime.interopservices.fieldoffsetattribute%29%0A%0AFor%20further%20assistance,%20please%20contact%20%5B.NET%20Breaking%20Change%20Notifications%5D%28mailto%3Adotnetbcn%40microsoft.com%29.&labels=breaking-change,Pri1,doc-idea)

*Generated by Breaking Change Documentation Tool - 2025-10-03 13:48:12*

author_association: MEMBER
user: login: ericstj
created_at: 2025-10-22T16:44:22Z
body: @jkoritzinsky could you help by creating the breaking change doc for this?  Just click the link above and review the issue and submit.  Thank you!
author_association: MEMBER
user: login: jkoritzinsky
created_at: 2025-10-22T16:45:34Z
body: We don't need a breaking change doc for this change. We changed the implementation such that it's not something we need a notice for. Let me remove the label.
author_association: MEMBER