Harden memory layer: fix char sizing, ref structs, count guards, ReadString advance

Second-review fixes, each covered by a regression test in MemoryHardeningTests:

- MarshalCache: special-case char (Size=2; Marshal.SizeOf reports 1/ANSI but the
  blittable path reads a 2-byte UTF-16 unit). TypeRequiresMarshal now also trips on
  RuntimeHelpers.IsReferenceOrContainsReferences<T>() so reference-carrying structs
  route to the marshal path instead of throwing in MemoryMarshal.Read. Document that
  the MarshalAs scan is top-level only.
- MemoryBase.Read<T>(count): reject negative count (ArgumentOutOfRangeException) and
  guard elementSize*count overflow. Same overflow guard on Write<T>(values).
- MemoryBase.ReadString: advance by bytes actually read, not the requested amount, so
  a partial read no longer skips the unread tail of the window.
- ExternalReader: default to a minimal access set (not AllAccess, which over-requests
  and fails on protected processes); wrap Process.MainModule in try/catch so a
  bitness-mismatched or protected target yields ImageBase=Zero instead of throwing.
- NativeMethods: WaitForSingleObject and CreateRemoteThread's threadId are DWORD (uint),
  not int — the signatures no longer sign-flip.

Deferred: hoisting the identical ExternalReader/InProcessReader byte-IO into MemoryBase
(cosmetic; skipped to avoid colliding with concurrent Phase 3 edits).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
kbe
2026-07-21 19:47:24 +02:00
co-authored by Claude Opus 4.8
parent f7236eea9b
commit 7c5e72e0e0
5 changed files with 251 additions and 15 deletions
+23 -4
View File
@@ -19,10 +19,19 @@ public static class MarshalCache<T>
public static readonly uint SizeU;
/// <summary>
/// <see langword="true"/> when <typeparamref name="T"/> has at least one field
/// decorated with <see cref="MarshalAsAttribute"/>, meaning it cannot be copied
/// via a simple pointer dereference.
/// <see langword="true"/> when <typeparamref name="T"/> cannot be copied through the
/// blittable <see cref="System.Runtime.InteropServices.MemoryMarshal"/> path and must
/// use <see cref="Marshal.PtrToStructure"/>/<see cref="Marshal.StructureToPtr"/> instead.
/// This is the case when a top-level field carries <see cref="MarshalAsAttribute"/>, or
/// when <typeparamref name="T"/> contains a managed reference
/// (<see cref="System.Runtime.CompilerServices.RuntimeHelpers.IsReferenceOrContainsReferences{T}"/>).
/// </summary>
/// <remarks>
/// The <see cref="MarshalAsAttribute"/> check inspects only top-level fields; a
/// <see cref="MarshalAsAttribute"/> on a field of a nested struct is not detected.
/// Reference-containing nested structs are still caught, because the reference check
/// propagates through nested value types.
/// </remarks>
public static readonly bool TypeRequiresMarshal;
/// <summary><see langword="true"/> when <typeparamref name="T"/> is <see cref="IntPtr"/>.</summary>
@@ -46,6 +55,13 @@ public static class MarshalCache<T>
Size = 1;
RealType = typeof(T);
}
else if (typeof(T) == typeof(char))
{
// Marshal.SizeOf(char) is 1 (ANSI), but the blittable path reads/writes a
// char as a 2-byte UTF-16 code unit. Size must match the blittable width.
Size = 2;
RealType = typeof(T);
}
else if (typeof(T).IsEnum)
{
Type underlying = typeof(T).GetEnumUnderlyingType();
@@ -62,8 +78,11 @@ public static class MarshalCache<T>
SizeU = (uint)Size;
IsIntPtr = RealType == typeof(IntPtr);
TypeRequiresMarshal =
bool hasMarshalAsField =
RealType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Any(f => f.GetCustomAttributes(typeof(MarshalAsAttribute), true).Length != 0);
TypeRequiresMarshal =
hasMarshalAsField || System.Runtime.CompilerServices.RuntimeHelpers.IsReferenceOrContainsReferences<T>();
}
}