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
+13 -5
View File
@@ -76,12 +76,16 @@ public abstract class MemoryBase : IDisposable
/// returns fewer bytes than expected.</returns>
public T[] Read<T>(IntPtr address, int count, bool isRelative = false) where T : struct
{
ArgumentOutOfRangeException.ThrowIfNegative(count);
if (isRelative)
address = GetAbsolute(address);
int elementSize = MarshalCache<T>.Size;
int totalSize = elementSize * count;
byte[] raw = ReadBytes(address, totalSize);
long totalSize = (long)elementSize * count;
ArgumentOutOfRangeException.ThrowIfGreaterThan(totalSize, int.MaxValue, nameof(count));
byte[] raw = ReadBytes(address, (int)totalSize);
int actualCount = Math.Min(count, raw.Length / elementSize);
var result = new T[actualCount];
@@ -124,7 +128,9 @@ public abstract class MemoryBase : IDisposable
return true;
int elementSize = MarshalCache<T>.Size;
int totalSize = elementSize * values.Length;
long total = (long)elementSize * values.Length;
ArgumentOutOfRangeException.ThrowIfGreaterThan(total, int.MaxValue, nameof(values));
int totalSize = (int)total;
byte[] raw = new byte[totalSize];
Span<byte> span = raw;
@@ -181,8 +187,10 @@ public abstract class MemoryBase : IDisposable
}
accumulated.Add(chunk);
address += take;
remaining -= take;
// Advance by the bytes actually read, not the amount requested: a partial
// read (chunk.Length < take) must not skip the unread tail of the window.
address += chunk.Length;
remaining -= chunk.Length;
}
int totalLength = 0;