[GH-ISSUE #3154] [Feature] Decode ECMA-335 custom attribute arguments from the constructor signature in inspect_managed_artifact #1101

Open
opened 2026-10-15 18:31:03 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @molako2 on GitHub (Oct 14, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/3154

Before submitting

  • I searched the open and closed issues and pull requests for a related proposal.
  • I understand the contribution scope must be reviewed before integration; open questions are welcome here.
  • This request does not include credentials, private samples, or sensitive target data.

Area

Other

Current limitation

inspect_managed_artifact decodes a custom attribute argument for one attribute type only: System.Runtime.Versioning.TargetFrameworkAttribute. Every other attribute reports decoded_fixed_string: null, even when its value blob is one well-formed string.

On 2026-10-14, the assemblies of a commercial Windows application were obfuscated. Their ORM mapping attributes, Dapper.Contrib.Extensions.TableAttribute and an application-defined ColumnAttribute, still carried the database table and column names as fixed string arguments. The ORM reads these strings at run time, so renaming them would break the mapping. In that application they were intact, and they were the most useful facts in those assemblies, and REA returned only value_length and value_sha256 for them.

Reproduction (synthetic, no vendor file)

Environment: Windows 11, csc.exe 4.8.9221.0, C# 5 (C:\Windows\Microsoft.NET\Framework64\v4.0.30319), rea-agents 6.4.0 MCP server. The source was read at commit 583008852.

The C# sources under Verification details build two assemblies:

  • Fixture.Attributes.dll defines Fixture.Orm.TableAttribute(string) and the byte enum Fixture.Orm.Storage. It stands in for a third-party ORM, so consumers reach its constructor through a MemberRef.
  • Fixture.Model.dll defines ColumnAttribute(string), which uses a MethodDef constructor, and ShapeAttribute(int, Type, object, string[]). Its class Customer carries these attributes.

inspect_managed_artifact { "path": "<abs>/Fixture.Model.dll" } returns coverage.state: "complete", 18 attributes, and decoded_fixed_string: null on all 18. The raw blobs come from #Blob (find_blobs.py, under Verification details):

Attribute Constructor Blob (hex)
Table("clients") 0x0a000006 MemberRef 01 00 07 63 6c 69 65 6e 74 73 00 00
Column("nom_client", Key = true) 0x06000001 MethodDef 01 00 0a 6e 6f 6d 5f 63 6c 69 65 6e 74 01 00 54 02 03 4b 65 79 01
Column(null) 0x06000001 MethodDef 01 00 ff 00 00
Storage(Storage.Disk) (external byte enum) 0x0a000007 MemberRef 01 00 02 00 00
Shape(42, typeof(string), "boxed", new[] { "a", null }) 0x06000004 MethodDef 01 00 2a 00 00 00 5a "System.String, mscorlib, Version=4.0.0.0, …" 0e 05 "boxed" 02 00 00 00 01 61 ff 00 00 (113 bytes)

Owning code

  • src/dotnet/ManagedMetadataInventoryRows.ts:423-426 readCustomAttribute calls the decoder only when type_name === "System.Runtime.Versioning.TargetFrameworkAttribute".
  • decodeFixedString (:324-344) checks the prolog 0x0001. It then assumes that the first fixed argument is a SerString and ignores the constructor signature, the remaining fixed arguments and the named arguments.
  • src/domain/managed/managedArtifact.ts:68-77 has one field, decoded_fixed_string: string | null. That null means four different things: not attempted, a null SerString (0xFF), invalid UTF-8, or a truncated blob. Column(null) and "not decoded" cannot be told apart.
  • src/dotnet/ManagedMetadataInventory.ts:163-171 is the only consumer. It derives target_frameworks from that field.
  • Constructor signatures are parsed in src/dotnet/ManagedMemberInspectorCore.ts:720 (decodeSignature, readTypeSignature, ELEMENT_TYPES at :289). That parser renders each parameter type as a display string ("string", a class name). It does not expose structured element types.

Why widening the name gate is wrong

A mirror of decodeFixedString (apply_current_decoder.mjs, under Verification details) applied to the blobs in the table gives:

Table("clients")                       "clients"
Column("nom_client", Key = true)       "nom_client"
Column(null)                           null
Storage(Storage.Disk), byte enum       "\u0000\u0000"
Shape(42, …)                           "\u0000\u0000\u0000ZSystem.String, mscorlib, Version=4.0.0"

The blob layout follows the constructor's parameter list (ECMA-335 II.23.3). The blob does not describe its own types. An allow-list of attribute names would emit wrong strings as soon as the first parameter is not a string. Decoding must follow the constructor signature.

Use cases

No response

Desired outcome and success criteria

Replace decoded_fixed_string with structured arguments decoded from the constructor MethodDefSig or MemberRefSig:

{ "type_name": "Fixture.Orm.TableAttribute", "constructor_token": "0x0a000006",
  "value_length": 12, "value_sha256": "b2da2b01…",
  "arguments": { "status": "decoded",
    "fixed": [ { "type": "string", "value": "clients", "blob_offset": 2 } ],
    "named": [] } }
  • Fixed arguments. Decode bool, char, int8 to uint64, float32, float64, string (null kept distinct from empty), System.Type (the SerString type name, kept verbatim) and boxed object (the FieldOrPropType tag plus its value). Also decode SZARRAY of these, keeping the null array (0xFFFFFFFF) and null elements. Report 64-bit integers as decimal strings so precision is not lost.
  • Named arguments. FIELD (0x53) or PROPERTY (0x54), the type, the name and the value. In the table, Column(…, Key = true) becomes { kind: "property", name: "Key", type: "bool", value: true }.
  • Enums. A local enum (TypeDef in the same module) resolves its underlying type from the value__ field signature. That result is a derivation and is labelled as one. An enum defined in another assembly (a TypeRef, such as Fixture.Orm.Storage) has an unknown width. The decoder must stop at that argument and mark it, and every following argument, as unknown, with the reason "enum underlying type is defined in assembly Fixture.Attributes, which was not inspected". Optional, for maintainers to decide: when the remaining byte count admits exactly one width, report that width as an inference next to the unknown.
  • Unknown shapes. Generic attribute type parameters (VAR/MVAR), an unresolved constructor, a malformed prolog, trailing bytes and truncation each give status: "partial", "unsupported" or "malformed" with a reason. They never give a guessed value. value_length and value_sha256 stay on every row.
  • One owner for signatures. ManagedMemberInspectorCore.ts exposes a structured parameter-type representation. Display rendering and the attribute decoder both consume it. AGENTS.md asks consumers to use the owner's parsed facts and to keep lookup values apart from display formatting, so a second signature parser in ManagedMetadataInventoryRows.ts is not acceptable.
  • No superseded field. target_frameworks is computed from the new arguments. decoded_fixed_string is removed, which is a breaking output change (! or a BREAKING CHANGE: footer), unless maintainers prefer a one-release deprecation.

Success: the Fixture.Model.dll workflow returns "clients", "nom_client", Key = true, a null string for Column(null), an explicit unknown for Storage, and all four Shape arguments, all with byte offsets inside the blob.

Expected interaction with targets

Read existing files or captured data

Target and scope details (optional)

Authority and side effects

  • Static and read-only. No CLR load, no System.Reflection, no MetadataLoadContext, no execution of attribute constructors.
  • No cross-file resolution. Referenced assemblies are not opened implicitly. Letting the caller select reference assemblies for enum resolution is a possible follow-up, out of scope here.
  • Resource bounds: charge every decoded string and array to managedDecodeBudget. Check an array count (up to 2^32 − 1) against the remaining blob bytes before allocating.

Verification available so far

I have a prototype or observed results

Verification details (optional)

  • In-repo fixture, no new builder needed. buildManagedMemberPointerFixture (src/dotnet/ManagedPe.fixture.ts:1143-1267) already emits an attribute with a MethodDef constructor (string) (signature 20 01 01 0e) and the blob fixedStringAttributeBlob("fixture"). With the name gate at line 424, its decoded_fixed_string should be null; this is derived from the code, the test was not run. The new expectation is fixed: [{ type: "string", value: "fixture" }].
  • Extend the fixture builder with:
    • a MemberRef constructor
    • a named property argument and a named field argument
    • a null string and an empty string
    • a local enum with value__, and an external enum given as a TypeRef
    • System.Type, a boxed object and an SZARRAY with a null element
    • malformed blobs: wrong prolog, truncation, trailing bytes, an array count beyond the blob, invalid UTF-8
  • Workflows. CLI and MCP inspect_managed_artifact goldens. The target_frameworks output for the existing fixtures must not change.
  • Optional oracle (CI with the .NET SDK). Compile the fixture sources and compare with System.Reflection.Metadata CustomAttribute.DecodeValue, used as a test oracle only and never at run time.
  • Unverified. Attributes on generic types, C# 11 generic attributes, and attributes in obfuscated assemblies with malformed signatures.

Performance

Real assemblies carry thousands of attribute rows. Most of them are empty-prolog rows, such as CompilerGeneratedAttribute (01 00 00 00): 9 of the 18 rows in the fixture. I propose to leave out arguments when a row has no fixed and no named arguments. A caller option to skip decoding can be added if measurements show a cost. Following AGENTS.md, report output tokens, elapsed time, CPU time and peak memory before and after, cold and warm, on one large public framework assembly (for example System.Private.CoreLib.dll from an installed .NET runtime). This is not measured yet.

Fixture generators (synthetic, no vendor file)

Attributes.cs
using System;
namespace Fixture.Orm {
  // Stands in for a third-party ORM attribute: consumers reach its .ctor through a MemberRef.
  [AttributeUsage(AttributeTargets.Class)]
  public sealed class TableAttribute : Attribute { public TableAttribute(string name) { Name = name; } public string Name { get; private set; } }
  public enum Storage : byte { Heap = 1, Disk = 2 }
  [AttributeUsage(AttributeTargets.All)]
  public sealed class StorageAttribute : Attribute { public StorageAttribute(Storage storage) { } }
}
Model.cs
using System;
using Fixture.Orm;
namespace Fixture.Model {
  // Local attribute: its .ctor is a MethodDef in this module.
  [AttributeUsage(AttributeTargets.Property)]
  public sealed class ColumnAttribute : Attribute { public ColumnAttribute(string name) { } public bool Key { get; set; } }
  [AttributeUsage(AttributeTargets.All)]
  public sealed class ShapeAttribute : Attribute { public ShapeAttribute(int n, Type t, object o, string[] tags) { } }

  [Table("clients")]
  [Storage(Storage.Disk)]
  [Shape(42, typeof(string), "boxed", new[] { "a", null })]
  public class Customer {
    [Column("nom_client", Key = true)] public string Name { get; set; }
    [Column(null)] public string Missing { get; set; }
  }
}
find_blobs.py
# Locate custom-attribute blobs (prolog 0x0001) by their compressed-length prefix in the #Blob heap.
import sys
data = open(sys.argv[1], "rb").read()
for length in map(int, sys.argv[2:]):
    needle = bytes([length]) + b"\x01\x00"
    at = data.find(needle)
    while at >= 0:
        blob = data[at + 1:at + 1 + length]
        print(length, at + 1, blob.hex(" "))
        at = data.find(needle, at + 1)
apply_current_decoder.mjs
// Mirrors decodeFixedString() in src/dotnet/ManagedMetadataInventoryRows.ts:324-344
// (REA 6.4.0, commit 583008852) to show what widening its type-name gate would
// emit when the constructor's first parameter is not a string.
// Blobs are the exact bytes compiled into Fixture.Model.dll by managed-src/.
const compressedLength = (blob, offset) => {
  const first = blob[offset];
  if (first === undefined) return undefined;
  if ((first & 0x80) === 0) return { length: first, prefix: 1 };
  if ((first & 0xc0) === 0x80 && blob[offset + 1] !== undefined)
    return { length: ((first & 0x3f) << 8) | blob[offset + 1], prefix: 2 };
  return undefined; // 4-byte form omitted: no fixture blob uses it
};
const decodeFixedString = (blob) => {
  if (blob.length < 3 || blob.readUInt16LE(0) !== 1 || blob[2] === 0xff) return null;
  const length = compressedLength(blob, 2);
  if (length === undefined) return null;
  const start = 2 + length.prefix;
  if (start > blob.length - length.length) return null;
  try {
    return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
      blob.subarray(start, start + length.length),
    );
  } catch {
    return null;
  }
};
const blobs = [
  ['Table("clients")', "010007636c69656e74730000"],
  ['Column("nom_client", Key = true)', "01000a6e6f6d5f636c69656e740100540203" + "4b657901"],
  ["Column(null)", "0100ff0000"],
  ["Storage(Storage.Disk), byte enum", "0100020000"],
  [
    'Shape(42, typeof(string), "boxed", new[] { "a", null })',
    "01002a0000005a53797374656d2e537472696e672c206d73636f726c69622c2056657273696f6e3d342e302e302e302c2043756c747572653d6e65757472616c2c205075626c69634b6579546f6b656e3d623737613563353631393334653038390e05626f78656402000000016" + "1ff0000",
  ],
];
for (const [name, hex] of blobs)
  console.log(name.padEnd(56), JSON.stringify(decodeFixedString(Buffer.from(hex, "hex"))));

Alternatives considered

  • Add Dapper.Contrib.Extensions.TableAttribute and similar names to the gate. Wrong for non-string first parameters, as the mirror output shows, and the list has no end.
  • Load the assembly with reflection. This executes or loads target code and depends on the host CLR. It is excluded by the static inspection contract.
  • Leave decoding to inspect_managed_members or decompilation. Neither answers the inventory question in one call, and both cost more tokens.

Possible direction

The plan under Desired outcome and success criteria is a proposal, not a requirement. No implementation is intended before a maintainer scope decision.

References

  • #1736 (TargetFrameworkAttribute decoding), #1940 (attribute constructor lookup cost)
  • ECMA-335 6th edition, partition II.23.3 (custom attributes) and II.23.2 (signatures)
Originally created by @molako2 on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3154 ### Before submitting - [X] I searched the open and closed issues and pull requests for a related proposal. - [X] I understand the [contribution scope](https://github.com/morluto/rea/blob/main/CONTRIBUTING.md#contribution-scope) must be reviewed before integration; open questions are welcome here. - [X] This request does not include credentials, private samples, or sensitive target data. ### Area Other ### Current limitation `inspect_managed_artifact` decodes a custom attribute argument for one attribute type only: `System.Runtime.Versioning.TargetFrameworkAttribute`. Every other attribute reports `decoded_fixed_string: null`, even when its value blob is one well-formed string. On 2026-10-14, the assemblies of a commercial Windows application were obfuscated. Their ORM mapping attributes, `Dapper.Contrib.Extensions.TableAttribute` and an application-defined `ColumnAttribute`, still carried the database table and column names as fixed string arguments. The ORM reads these strings at run time, so renaming them would break the mapping. In that application they were intact, and they were the most useful facts in those assemblies, and REA returned only `value_length` and `value_sha256` for them. #### Reproduction (synthetic, no vendor file) Environment: Windows 11, `csc.exe` 4.8.9221.0, C# 5 (`C:\Windows\Microsoft.NET\Framework64\v4.0.30319`), `rea-agents` 6.4.0 MCP server. The source was read at commit 583008852. The C# sources under *Verification details* build two assemblies: - `Fixture.Attributes.dll` defines `Fixture.Orm.TableAttribute(string)` and the byte enum `Fixture.Orm.Storage`. It stands in for a third-party ORM, so consumers reach its constructor through a `MemberRef`. - `Fixture.Model.dll` defines `ColumnAttribute(string)`, which uses a `MethodDef` constructor, and `ShapeAttribute(int, Type, object, string[])`. Its class `Customer` carries these attributes. `inspect_managed_artifact { "path": "<abs>/Fixture.Model.dll" }` returns `coverage.state: "complete"`, 18 attributes, and `decoded_fixed_string: null` on all 18. The raw blobs come from `#Blob` (`find_blobs.py`, under *Verification details*): | Attribute | Constructor | Blob (hex) | |---|---|---| | `Table("clients")` | `0x0a000006` MemberRef | `01 00 07 63 6c 69 65 6e 74 73 00 00` | | `Column("nom_client", Key = true)` | `0x06000001` MethodDef | `01 00 0a 6e 6f 6d 5f 63 6c 69 65 6e 74 01 00 54 02 03 4b 65 79 01` | | `Column(null)` | `0x06000001` MethodDef | `01 00 ff 00 00` | | `Storage(Storage.Disk)` (external byte enum) | `0x0a000007` MemberRef | `01 00 02 00 00` | | `Shape(42, typeof(string), "boxed", new[] { "a", null })` | `0x06000004` MethodDef | `01 00 2a 00 00 00 5a "System.String, mscorlib, Version=4.0.0.0, …" 0e 05 "boxed" 02 00 00 00 01 61 ff 00 00` (113 bytes) | #### Owning code - [`src/dotnet/ManagedMetadataInventoryRows.ts:423-426`](https://github.com/morluto/rea/blob/58300885207133422af235e0f2b10658cf958b03/src/dotnet/ManagedMetadataInventoryRows.ts#L423-L426) `readCustomAttribute` calls the decoder only when `type_name === "System.Runtime.Versioning.TargetFrameworkAttribute"`. - `decodeFixedString` (`:324-344`) checks the prolog `0x0001`. It then assumes that the first fixed argument is a `SerString` and ignores the constructor signature, the remaining fixed arguments and the named arguments. - [`src/domain/managed/managedArtifact.ts:68-77`](https://github.com/morluto/rea/blob/58300885207133422af235e0f2b10658cf958b03/src/domain/managed/managedArtifact.ts#L68-L77) has one field, `decoded_fixed_string: string | null`. That `null` means four different things: not attempted, a null `SerString` (`0xFF`), invalid UTF-8, or a truncated blob. `Column(null)` and "not decoded" cannot be told apart. - [`src/dotnet/ManagedMetadataInventory.ts:163-171`](https://github.com/morluto/rea/blob/58300885207133422af235e0f2b10658cf958b03/src/dotnet/ManagedMetadataInventory.ts#L163-L171) is the only consumer. It derives `target_frameworks` from that field. - Constructor signatures are parsed in [`src/dotnet/ManagedMemberInspectorCore.ts:720`](https://github.com/morluto/rea/blob/58300885207133422af235e0f2b10658cf958b03/src/dotnet/ManagedMemberInspectorCore.ts#L720) (`decodeSignature`, `readTypeSignature`, `ELEMENT_TYPES` at `:289`). That parser renders each parameter type as a display string (`"string"`, a class name). It does not expose structured element types. #### Why widening the name gate is wrong A mirror of `decodeFixedString` (`apply_current_decoder.mjs`, under *Verification details*) applied to the blobs in the table gives: ```text Table("clients") "clients" Column("nom_client", Key = true) "nom_client" Column(null) null Storage(Storage.Disk), byte enum "\u0000\u0000" Shape(42, …) "\u0000\u0000\u0000ZSystem.String, mscorlib, Version=4.0.0" ``` The blob layout follows the constructor's parameter list (ECMA-335 II.23.3). The blob does not describe its own types. An allow-list of attribute names would emit wrong strings as soon as the first parameter is not a `string`. Decoding must follow the constructor signature. ### Use cases _No response_ ### Desired outcome and success criteria Replace `decoded_fixed_string` with structured arguments decoded from the constructor `MethodDefSig` or `MemberRefSig`: ```json { "type_name": "Fixture.Orm.TableAttribute", "constructor_token": "0x0a000006", "value_length": 12, "value_sha256": "b2da2b01…", "arguments": { "status": "decoded", "fixed": [ { "type": "string", "value": "clients", "blob_offset": 2 } ], "named": [] } } ``` - **Fixed arguments.** Decode `bool`, `char`, `int8` to `uint64`, `float32`, `float64`, `string` (null kept distinct from empty), `System.Type` (the `SerString` type name, kept verbatim) and boxed `object` (the `FieldOrPropType` tag plus its value). Also decode `SZARRAY` of these, keeping the null array (`0xFFFFFFFF`) and null elements. Report 64-bit integers as decimal strings so precision is not lost. - **Named arguments.** `FIELD` (`0x53`) or `PROPERTY` (`0x54`), the type, the name and the value. In the table, `Column(…, Key = true)` becomes `{ kind: "property", name: "Key", type: "bool", value: true }`. - **Enums.** A local enum (`TypeDef` in the same module) resolves its underlying type from the `value__` field signature. That result is a derivation and is labelled as one. An enum defined in another assembly (a `TypeRef`, such as `Fixture.Orm.Storage`) has an unknown width. The decoder must stop at that argument and mark it, and every following argument, as `unknown`, with the reason "enum underlying type is defined in assembly Fixture.Attributes, which was not inspected". Optional, for maintainers to decide: when the remaining byte count admits exactly one width, report that width as an **inference** next to the unknown. - **Unknown shapes.** Generic attribute type parameters (`VAR`/`MVAR`), an unresolved constructor, a malformed prolog, trailing bytes and truncation each give `status: "partial"`, `"unsupported"` or `"malformed"` with a reason. They never give a guessed value. `value_length` and `value_sha256` stay on every row. - **One owner for signatures.** `ManagedMemberInspectorCore.ts` exposes a structured parameter-type representation. Display rendering and the attribute decoder both consume it. `AGENTS.md` asks consumers to use the owner's parsed facts and to keep lookup values apart from display formatting, so a second signature parser in `ManagedMetadataInventoryRows.ts` is not acceptable. - **No superseded field.** `target_frameworks` is computed from the new `arguments`. `decoded_fixed_string` is removed, which is a breaking output change (`!` or a `BREAKING CHANGE:` footer), unless maintainers prefer a one-release deprecation. Success: the `Fixture.Model.dll` workflow returns `"clients"`, `"nom_client"`, `Key = true`, a null string for `Column(null)`, an explicit unknown for `Storage`, and all four `Shape` arguments, all with byte offsets inside the blob. ### Expected interaction with targets Read existing files or captured data ### Target and scope details (optional) #### Authority and side effects - Static and read-only. No CLR load, no `System.Reflection`, no `MetadataLoadContext`, no execution of attribute constructors. - No cross-file resolution. Referenced assemblies are not opened implicitly. Letting the caller select reference assemblies for enum resolution is a possible follow-up, out of scope here. - Resource bounds: charge every decoded string and array to `managedDecodeBudget`. Check an array count (up to `2^32 − 1`) against the remaining blob bytes **before** allocating. ### Verification available so far I have a prototype or observed results ### Verification details (optional) - **In-repo fixture, no new builder needed.** `buildManagedMemberPointerFixture` ([`src/dotnet/ManagedPe.fixture.ts:1143-1267`](https://github.com/morluto/rea/blob/58300885207133422af235e0f2b10658cf958b03/src/dotnet/ManagedPe.fixture.ts#L1143-L1267)) already emits an attribute with a `MethodDef` constructor `(string)` (signature `20 01 01 0e`) and the blob `fixedStringAttributeBlob("fixture")`. With the name gate at line 424, its `decoded_fixed_string` should be `null`; this is derived from the code, the test was not run. The new expectation is `fixed: [{ type: "string", value: "fixture" }]`. - **Extend the fixture builder** with: - a `MemberRef` constructor - a named property argument and a named field argument - a null string and an empty string - a local enum with `value__`, and an external enum given as a `TypeRef` - `System.Type`, a boxed `object` and an `SZARRAY` with a null element - malformed blobs: wrong prolog, truncation, trailing bytes, an array count beyond the blob, invalid UTF-8 - **Workflows.** CLI and MCP `inspect_managed_artifact` goldens. The `target_frameworks` output for the existing fixtures must not change. - **Optional oracle (CI with the .NET SDK).** Compile the fixture sources and compare with `System.Reflection.Metadata` `CustomAttribute.DecodeValue`, used as a test oracle only and never at run time. - **Unverified.** Attributes on generic types, C# 11 generic attributes, and attributes in obfuscated assemblies with malformed signatures. #### Performance Real assemblies carry thousands of attribute rows. Most of them are empty-prolog rows, such as `CompilerGeneratedAttribute` (`01 00 00 00`): 9 of the 18 rows in the fixture. I propose to leave out `arguments` when a row has no fixed and no named arguments. A caller option to skip decoding can be added if measurements show a cost. Following `AGENTS.md`, report output tokens, elapsed time, CPU time and peak memory before and after, cold and warm, on one large public framework assembly (for example `System.Private.CoreLib.dll` from an installed .NET runtime). This is not measured yet. #### Fixture generators (synthetic, no vendor file) <details><summary><code>Attributes.cs</code></summary> ```csharp using System; namespace Fixture.Orm { // Stands in for a third-party ORM attribute: consumers reach its .ctor through a MemberRef. [AttributeUsage(AttributeTargets.Class)] public sealed class TableAttribute : Attribute { public TableAttribute(string name) { Name = name; } public string Name { get; private set; } } public enum Storage : byte { Heap = 1, Disk = 2 } [AttributeUsage(AttributeTargets.All)] public sealed class StorageAttribute : Attribute { public StorageAttribute(Storage storage) { } } } ``` </details> <details><summary><code>Model.cs</code></summary> ```csharp using System; using Fixture.Orm; namespace Fixture.Model { // Local attribute: its .ctor is a MethodDef in this module. [AttributeUsage(AttributeTargets.Property)] public sealed class ColumnAttribute : Attribute { public ColumnAttribute(string name) { } public bool Key { get; set; } } [AttributeUsage(AttributeTargets.All)] public sealed class ShapeAttribute : Attribute { public ShapeAttribute(int n, Type t, object o, string[] tags) { } } [Table("clients")] [Storage(Storage.Disk)] [Shape(42, typeof(string), "boxed", new[] { "a", null })] public class Customer { [Column("nom_client", Key = true)] public string Name { get; set; } [Column(null)] public string Missing { get; set; } } } ``` </details> <details><summary><code>find_blobs.py</code></summary> ```python # Locate custom-attribute blobs (prolog 0x0001) by their compressed-length prefix in the #Blob heap. import sys data = open(sys.argv[1], "rb").read() for length in map(int, sys.argv[2:]): needle = bytes([length]) + b"\x01\x00" at = data.find(needle) while at >= 0: blob = data[at + 1:at + 1 + length] print(length, at + 1, blob.hex(" ")) at = data.find(needle, at + 1) ``` </details> <details><summary><code>apply_current_decoder.mjs</code></summary> ```javascript // Mirrors decodeFixedString() in src/dotnet/ManagedMetadataInventoryRows.ts:324-344 // (REA 6.4.0, commit 583008852) to show what widening its type-name gate would // emit when the constructor's first parameter is not a string. // Blobs are the exact bytes compiled into Fixture.Model.dll by managed-src/. const compressedLength = (blob, offset) => { const first = blob[offset]; if (first === undefined) return undefined; if ((first & 0x80) === 0) return { length: first, prefix: 1 }; if ((first & 0xc0) === 0x80 && blob[offset + 1] !== undefined) return { length: ((first & 0x3f) << 8) | blob[offset + 1], prefix: 2 }; return undefined; // 4-byte form omitted: no fixture blob uses it }; const decodeFixedString = (blob) => { if (blob.length < 3 || blob.readUInt16LE(0) !== 1 || blob[2] === 0xff) return null; const length = compressedLength(blob, 2); if (length === undefined) return null; const start = 2 + length.prefix; if (start > blob.length - length.length) return null; try { return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode( blob.subarray(start, start + length.length), ); } catch { return null; } }; const blobs = [ ['Table("clients")', "010007636c69656e74730000"], ['Column("nom_client", Key = true)', "01000a6e6f6d5f636c69656e740100540203" + "4b657901"], ["Column(null)", "0100ff0000"], ["Storage(Storage.Disk), byte enum", "0100020000"], [ 'Shape(42, typeof(string), "boxed", new[] { "a", null })', "01002a0000005a53797374656d2e537472696e672c206d73636f726c69622c2056657273696f6e3d342e302e302e302c2043756c747572653d6e65757472616c2c205075626c69634b6579546f6b656e3d623737613563353631393334653038390e05626f78656402000000016" + "1ff0000", ], ]; for (const [name, hex] of blobs) console.log(name.padEnd(56), JSON.stringify(decodeFixedString(Buffer.from(hex, "hex")))); ``` </details> ### Alternatives considered - **Add `Dapper.Contrib.Extensions.TableAttribute` and similar names to the gate.** Wrong for non-string first parameters, as the mirror output shows, and the list has no end. - **Load the assembly with reflection.** This executes or loads target code and depends on the host CLR. It is excluded by the static inspection contract. - **Leave decoding to `inspect_managed_members` or decompilation.** Neither answers the inventory question in one call, and both cost more tokens. ### Possible direction The plan under *Desired outcome and success criteria* is a proposal, not a requirement. No implementation is intended before a maintainer scope decision. ### References - #1736 (TargetFrameworkAttribute decoding), #1940 (attribute constructor lookup cost) - ECMA-335 6th edition, partition II.23.3 (custom attributes) and II.23.2 (signatures)
Author
Owner

@elricfd commented on GitHub (Oct 19, 2026):

Another data point on 6.7.0 (Windows 11 x64), from a large commercial .NET Framework app (Mixed In Key 11, MixedInKey.exe, SHA-256 b014a00e6f6b80ed9313f68ad3da8f42c2de38f9111582e62145cec63c7da843).

Observed: rea inspect-managed-artifact <exe> --json returns attributes with type_name: "System.Runtime.InteropServices.GuidAttribute" for 11 COM interop types, each with value_length: 41 and decoded_fixed_string: null. Only 1 of 6055 attributes has a non-null decoded_fixed_string.

Inference (not verified by reading the blob): 41 = 2 (prolog) + 1 (length) + 36 + 2 (named-argument count), consistent with a single 36-character string argument, so these GUIDs look decodable by the same fixed-string path.

Source-derived: the decode is gated on the attribute type name in src/dotnet/ManagedMetadataInventoryRows.ts (around lines 440-443; TargetFrameworkAttribute only), which is what this issue and #3317 address. GUIDs on COM interop types are a second concrete case next to the ORM attributes in the description.

<!-- gh-comment-id:6257687736 --> @elricfd commented on GitHub (Oct 19, 2026): Another data point on 6.7.0 (Windows 11 x64), from a large commercial .NET Framework app (Mixed In Key 11, `MixedInKey.exe`, SHA-256 `b014a00e6f6b80ed9313f68ad3da8f42c2de38f9111582e62145cec63c7da843`). Observed: `rea inspect-managed-artifact <exe> --json` returns `attributes` with `type_name: "System.Runtime.InteropServices.GuidAttribute"` for 11 COM interop types, each with `value_length: 41` and `decoded_fixed_string: null`. Only 1 of 6055 attributes has a non-null `decoded_fixed_string`. Inference (not verified by reading the blob): 41 = 2 (prolog) + 1 (length) + 36 + 2 (named-argument count), consistent with a single 36-character string argument, so these GUIDs look decodable by the same fixed-string path. Source-derived: the decode is gated on the attribute type name in `src/dotnet/ManagedMetadataInventoryRows.ts` (around lines 440-443; `TargetFrameworkAttribute` only), which is what this issue and #3317 address. GUIDs on COM interop types are a second concrete case next to the ORM attributes in the description.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#1101
No description provided.