Skip to content

fix: correct four defects in Memory and verify them in CI - #11

Merged
ayuthmang merged 1 commit into
masterfrom
claude/project-build-pipeline-6zecpc
Sep 5, 2026
Merged

ayuthmang merged 1 commit into
masterfrom
claude/project-build-pipeline-6zecpc

Conversation

@ayuthmang

@ayuthmang ayuthmang commented Sep 2, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fixes four defects in Memory.cs and turns MemoryXTest into a harness that exercises the library on every build.

CI is green on 7435c68: all 17 checks pass in both Debug and Release.

The harness

MemoryXTest was a demo hard-coded to Tutorial-x86_64 and fixed addresses — it compiled but could never run, so CI only ever proved the code parsed.

It now commits one page inside its own process, opens that process through the library, round-trips every read and write overload against the page, and exits non-zero on any failed check. No external target process, no elevation, so the build workflow runs it directly. Nothing is lost from the old file: the README already carries every usage example it demonstrated.

The defects

1. WriteMemory(long, byte) never wrote one byte

return WriteMemory(lpBaseAddress, BitConverter.GetBytes(value));

BitConverter.GetBytes has no byte overload. The argument was promoted to short (C# prefers a signed conversion target over unsigned), so writing one byte clobbered the following byte.

The README example was worse still — 0xba is an int literal, so it bound to the int overload and wrote four bytes. Fixed by building the single-byte buffer directly; the README now casts.

2. ReadSingle requested 8 bytes for a 4-byte value

ReadInt32 correctly asks for 4; ReadSingle asked for 8. When the extra 4 bytes crossed into unreadable memory the whole read failed and the method silently returned 0f.

3. GetProcessHandle reported success on failure

Both overloads returned true whenever no exception was thrown — including when OpenProcess returned NULL for access denied, which is exactly the case the README warns about. Callers got true, then every read returned zeros. They now return whether the handle is non-null.

4. GetBaseAddress bound an unused exception variable

catch (Exception ex) — the CS0168 warning present in every build log.

What the harness actually covers

Worth being precise, since a passing suite is only as good as its ability to fail:

Defect Regression coverage
1 — single-byte write Yes. Fills 4 bytes with 0xFF, writes one byte, asserts the other three survive. The old code wrote {0xBA, 0x00}, so after[1] would be 0x00 and the check fails. GetBytesWritten would also report 2.
2 — ReadSingle over-read Yes. Places a float in the last 4 bytes of a one-page allocation. VirtualAlloc reserves at 64 KB granularity, so the next page is free; an 8-byte read spans into it, fails outright, and returns 0f.
3 — false success No. The harness asserts an impossible process id is rejected, but the old code passed that too — it fails on the exception path, which was already correct. Provoking a real OpenProcess failure needs a process the runner cannot open, which is not reliable in CI. This fix rests on review, not on a test.
4 — CS0168 Not applicable; a compile-time warning with no runtime behaviour.

Also

MemoryXTest Release builds now set Prefer32Bit=false. Memory declares lpBaseAddress as a 64-bit long, which only matches the native LPVOID parameter in a 64-bit process; an AnyCPU executable otherwise defaults to running 32-bit, where these signatures are wrong. Debug already set this — Release did not.

Behaviour change

Fix 1 changes what a published method does: one byte written where two or four went before. It is what the method signature and the README both promise, and writing past the requested address is memory corruption in the target process. Say the word if you would rather keep the old overload and add a separate WriteByte.

Deliberately not included

Two items from the review need a maintainer decision rather than a silent change:

  • Pointer traversal is 32-bit only. ReadMemoryPointerInt, WriteMemoryPointer and GetPointerAddress walk chains with ReadInt32, truncating any pointer above 4 GB — yet the README's worked example targets a 64-bit process. Switching to 64-bit reads unconditionally would break 32-bit targets, so this needs a design decision (bitness detection, or explicit 32/64 variants).
  • WriteMemory(string) encodes ASCII while ReadString decodes UTF-8, and ReadString does not stop at a NUL.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SKZ2gugmpaYSHPS7LR5Xjg

MemoryXTest was a demo hard-coded to a specific game's process and
addresses, so it could compile but never run. Rewrite it as a self-verifying
harness: it commits a page inside its own process, opens that process through
the library, round-trips every read and write overload, and exits non-zero on
any failed check. That needs no external target and no elevation, so the
build workflow now runs it and CI checks behaviour rather than only
compilation.

The defects it covers:

- WriteMemory(long, byte) forwarded to BitConverter.GetBytes, which has no
  byte overload. The argument was promoted to short, so writing one byte
  clobbered the following byte. The README example was worse: 0xba is an int
  literal, so it bound to the int overload and wrote four bytes. Build the
  single-byte buffer directly, and correct the README to cast.

- ReadSingle requested 8 bytes for a 4-byte value, so a float near the end of
  a committed region failed to read and silently returned 0. Request 4, as
  ReadInt32 already does. The harness places a float in the last 4 bytes of a
  one-page allocation, which fails without this fix.

- Both GetProcessHandle overloads returned true whenever no exception was
  thrown, including when OpenProcess returned NULL for access denied. Callers
  then saw every read return zeros. Return whether the handle is non-null.

- GetBaseAddress bound an exception variable it never used (CS0168).

Release builds of MemoryXTest also now set Prefer32Bit=false. Memory declares
lpBaseAddress as a 64-bit long, which only matches the native LPVOID
parameter in a 64-bit process; an AnyCPU executable otherwise defaults to
running as 32-bit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKZ2gugmpaYSHPS7LR5Xjg
@ayuthmang
ayuthmang marked this pull request as ready for review September 2, 2026 18:24
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@ayuthmang
ayuthmang merged commit 5adccfc into master Sep 5, 2026
2 checks passed
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