Skip to content

fix: technical review findings across library, backends, app, mcp and ci - #111

Merged
JeanExtreme002 merged 4 commits into
mainfrom
fix/technical-review-findings
Oct 12, 2026
Merged

JeanExtreme002 merged 4 commits into
mainfrom
fix/technical-review-findings

Conversation

@JeanExtreme002

@JeanExtreme002 JeanExtreme002 commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Why is this PR necessary, what does it do?

A full technical review of the project (library, backends, MCP server, app, CI) produced a list of findings. Each one was re-verified against the code before changing anything; the ones that turned out not to be problems were left alone (see Notes). Public defaults are unchanged: where a finding could only be fully fixed by changing a default, this PR adds an opt-in safeguard and documents the risk instead. Pure refactors (deduplicating the backends, splitting large classes) are out of scope.

Library (process/, util/)

  • write_uint* accepted floats and truncated them (write_uint(a, 3.7) wrote 3) while write_int raised TypeError; unsigned writers now use operator.index. NumPy integers out of range were silently wrapped; they are range-checked now. ProcessInfo.pid = True opened pid 1; bools are rejected.
  • Search arguments are validated when search_by_value / search_by_value_between / search_by_pattern are called (new prepare_search_target), not on the first next() of the returned generator.
  • Variable-width regex scans reported a match's suffix again across a chunk boundary; each chunk now resumes where the last match ended. The chunk overlap could read past the end of a region; it is clamped.
  • The unaligned pointer map had no chunk overlap and missed pointers straddling a boundary. intersect_pointer_paths merged 32- and 64-bit paths with the same recipe; it keys on ptr_size too (JSON format unchanged).
  • resolve_pointer_chain: ctypes masks addresses to 64 bits, so NULL + negative offset read a wrapped address. A hop outside [0, 2**64) now raises an error that is both ValueError and OSError (what the failed read used to raise), so existing except OSError keeps working.
  • NumPy paths for the pointer map (16 MB: 0.46 s → 0.08 s, identical maps) and integer NOT_EXACT scans. Debug logs where a pointer-map chunk or the fixed-width check was skipped silently.

Backends (win32/, linux/, macos/)

  • Linux: /proc/*/comm, task comm and /proc/<pid>/maps were read in strict text mode, so one process with a non-UTF-8 name (any process can set one with prctl) broke OpenProcess(name=...) for everyone, and a non-UTF-8 mapped path broke every scan of that target. They are decoded with replacement characters now.
  • All: scan and listing generators captured the raw handle / task port and checked "open" only at creation. After p1.close(), Windows can hand the same handle value to the next OpenProcess, so a kept generator could read another process. New process/_guard.py re-checks before every resume and raises ClosedProcess.
  • Windows: a short ReadProcessMemory kept a zero-filled tail that could match a scan for 0 (now skipped like on Linux/macOS); get_modules retries (with a short pause) on ERROR_BAD_LENGTH and raises ProcessIDNotExistsError for an exited target; GetMappedFileNameW grows its buffer instead of truncating; unused system_information / _HOST_IS_64BIT removed.
  • macOS: VM_PROT_COPY left WRITE in a page's maximum protection after a write; the restore lowers it again. close() reports a failed port release (as Windows/Linux do); get_modules raises ProcessIDNotExistsError for an exited target; the region walk no longer wraps the 64-bit cursor or stops at a zero-size region; load commands are parsed once in _static_image_ranges.

App (app/)

  • Scan workers were never deleted (parent=self, no deleteLater), keeping every scan's address copies alive; closed tool dialogs had no WA_DeleteOnClose. Both are freed now.
  • A cancelled or failed first scan started an automatic "Updating values…" refine (two error dialogs on a dead target); the refresh now runs only after a completed scan.
  • Promoting N results did N full table rebuilds, and every refine chunk rebuilt the whole row index; both are linear now (refine of 200k rows: 18.7 s → 0.7 s).
  • Inline edit on a frozen row was overwritten by the stale frozen value for one tick; inline edit against a dead target kept the typed text silently (now reverted with a message, like bulk edit).
  • Pointer scan: Stop reported "No pointer paths found"; rows_ready had no backpressure; Compare walked a saved path's previous-run base when its module is not loaded, and the row could still be sent to the cheat table — such rows are shown unresolved and refused when acted on.
  • snapshot_ready disconnected on shutdown; swallowed shutdown exceptions are logged at debug.

MCP (mcp/)

  • Self-protection covered only getpid() / getppid(); under uvx the parent is uv, so the MCP client was attachable. New mcp/procinfo.py walks the ancestor chain (bounded, cycle- and pid-reuse-aware) and the policy protects all of it.
  • A remembered approval for python3 / node / shells approved every process with that name; for shared runtimes it now covers that instance only (approval_scope in the result), pinned to the start time shown in the prompt — if the pid is reused while the prompt is open, the attach is refused. Unreadable process names skipped the denylist; they always ask now. More password managers are denied; browsers and AI clients are flagged sensitive and warned about in the prompt (not denied).
  • Opt-in --confirm-writes: the server elicits before each write_value (fails closed). Off by default so clients that honour requiresUserInteraction are not asked twice; the risk of the default is documented.
  • Text values of str/bytes scans share a byte budget (additive *_truncated fields; hex is cut to an even length); tool descriptions mark memory contents as untrusted. Huge regions are scanned in pieces so the deadline holds.
  • bufflength="0" errored instead of meaning "default"; read_value / write_value echoed the raw value_type; find_scan read without the store lock; HTTP help text claimed remote hosting.

CI and packaging

  • python-package.yml gets top-level permissions: contents: read.
  • publish.yml is split into build (no id-token) and publish jobs, and requires a successful non-PR Python Package run on the release commit before uploading.
  • New CodeQL workflow. pip-audit audits only the extras' frozen closure (not its own dependencies), retries transient failures, and blocks outside pull requests.
  • mypy runs for linux, win32 and darwin with check_untyped_defs; macOS matrix covers 3.10, 3.12 and 3.14; informational app coverage.
  • psutil>=5.9,<8 (7.x verified), PySide6>=6.5,<7, tests extra refreshed; Hypothesis ci profile; make publish guarded by check-release-tag.
  • CONTRIBUTING: versioning policy, release process, running one platform's tests, corrected fast-lane description. SECURITY.md: MCP scope.

User-visible changes (for the release notes)

  • Resuming a scan/listing generator after close() raises ClosedProcess.
  • Invalid scan arguments raise at call time.
  • write_uint* rejects floats and numeric strings; OpenProcess(pid=True) is rejected.
  • Out-of-range pointer-chain hops raise ValueError (also an OSError).
  • macOS/Windows get_modules() on an exited target raise ProcessIDNotExistsError; macOS close() can return False.
  • Windows: a partially read scan chunk is skipped instead of zero-filled.
  • MCP: ancestors (including the shell/terminal it was started from) are refused; shared-runtime approvals are per instance (and refused if the pid is reused during the prompt); unreadable names always ask; new --confirm-writes; truncation fields and approval_scope / sensitive / write_confirmation.
  • App: no auto-refresh after cancel/error, clearer stop/dead-target messages, much faster promotion and refine.

Checklist (complete all items):

  • Added tests as necessary.
  • There is no breaking change for existing features.

References:

No references to be shared.

Notes:

  • Verified locally on macOS: 907 library/app tests (slow included) and 902 MCP tests pass; flake8, mypy (--platform darwin/linux/win32), sphinx -W, python -m build and actionlint are clean. MCP suite also passes on mcp 2.0.0, so the mcp>=2,<3 floor stays.
  • The new Linux and Windows code paths were exercised here only through ctypes-level fakes; the CI matrix is the real check for them.
  • Not changed on purpose: making read/write errors subclasses of PyMemoryEditorError (it would change what except PyMemoryEditorError catches and abort pointer scans on the first unreadable page); shrinking the Linux region struct's 4 KiB inline path (public field); returning the valid prefix of a partial read (needs a coordinated driver + 3-backend change); memoising dead ends in the reverse pointer walk (not provably result-identical); moving Compare / region snapshot off the UI thread (needs new workers).
  • Findings verified as not real: backends re-sorting in the core path, missing top-level exports, MCP refine against a newer region snapshot, progress-callback abort swallowed, the mcp floor, _set_busy bypass. The Linux freeze-after-exit concern is already covered by the pidfd binding from v3.2.0.

Library and backends:
- Linux: decode /proc names and map paths without failing on invalid UTF-8
- bind scan and listing generators to the process object (ClosedProcess after close)
- validate search arguments at call time, not on the first next()
- reject floats in unsigned writers and bool pids; range-check NumPy integers
- fix duplicate regex matches across chunks, overlap past region end and
  unaligned pointers straddling chunks; keep pointer widths apart in intersect
- refuse pointer-chain hops outside the 64-bit space (ValueError and OSError)
- Windows: drop short reads, retry module snapshots, grow mapped-file buffer
- macOS: restore max protection after writes, report failed close, walk fixes
- NumPy paths for the pointer map and integer NOT_EXACT scans

App:
- free finished workers and closed dialogs; no auto-refresh after cancel/error
- linear promotion and refine; frozen inline edits; dead-target inline edits
- pointer scan stop message, backpressure and unresolved compare rows

MCP:
- protect the whole launch chain; per-instance approvals for shared runtimes
- unreadable names always ask; more password managers denied, sensitive flag
- opt-in --confirm-writes; response size budget; huge regions scanned in pieces
- bufflength "0" sentinel, normalized value_type, locked scan lookup

CI and packaging:
- least-privilege permissions, split build/publish with a green-CI gate
- CodeQL, scoped and retried pip-audit gate, per-platform mypy, wider macOS matrix
- psutil <8, PySide6 <7, refreshed tests extra, release docs in CONTRIBUTING
@github-actions github-actions Bot added docs Documentation changes (docs/) app GUI application changes (PyMemoryEditor/app/) linux Linux backend changes (PyMemoryEditor/linux/) win32 Windows backend changes (PyMemoryEditor/win32/) macOS macOS backend changes (PyMemoryEditor/macos/) lib Library changes (PyMemoryEditor/) tests Test changes (tests/) mcp MCP changes (PyMemoryEditor/mcp) labels Oct 12, 2026
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.06383% with 63 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.41%. Comparing base (0a8f701) to head (9c72b99).

Files with missing lines Patch % Lines
PyMemoryEditor/app/main_window.py 62.16% 14 Missing ⚠️
PyMemoryEditor/app/pointer_scan_dialog.py 77.08% 11 Missing ⚠️
PyMemoryEditor/macos/functions.py 79.16% 10 Missing ⚠️
PyMemoryEditor/mcp/procinfo.py 88.46% 9 Missing ⚠️
PyMemoryEditor/mcp/server.py 95.08% 3 Missing ⚠️
PyMemoryEditor/mcp/toolset.py 95.89% 3 Missing ⚠️
PyMemoryEditor/app/application.py 0.00% 2 Missing ⚠️
PyMemoryEditor/app/cheat_poll_worker.py 71.42% 2 Missing ⚠️
PyMemoryEditor/macos/process.py 91.66% 2 Missing ⚠️
PyMemoryEditor/process/scanning.py 87.50% 2 Missing ⚠️
... and 4 more
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             main     #111       +/-   ##
===========================================
- Coverage   92.15%   77.41%   -14.75%     
===========================================
  Files          41       64       +23     
  Lines        4231     9186     +4955     
===========================================
+ Hits         3899     7111     +3212     
- Misses        332     2075     +1743     
Flag Coverage Δ
Linux-py3.10 66.94% <50.48%> (-0.47%) ⬇️
Linux-py3.11 66.94% <50.48%> (-0.47%) ⬇️
Linux-py3.12 66.94% <50.48%> (-0.47%) ⬇️
Linux-py3.13 66.94% <50.48%> (-0.47%) ⬇️
Linux-py3.14 66.85% <50.48%> (-0.46%) ⬇️
Linux-py3.15 66.85% <50.48%> (-0.46%) ⬇️
Windows-py3.10 68.53% <56.30%> (-0.17%) ⬇️
Windows-py3.11 68.53% <56.30%> (-0.17%) ⬇️
Windows-py3.12 68.53% <56.30%> (-0.17%) ⬇️
Windows-py3.13 68.53% <56.30%> (-0.09%) ⬇️
Windows-py3.14 68.44% <56.30%> (-0.16%) ⬇️
Windows-py3.15 68.44% <56.30%> (-0.16%) ⬇️
app-Linux-py3.12 61.58% <81.21%> (?)
macOS-py3.10 91.08% <90.60%> (?)
macOS-py3.12 91.03% <90.60%> (+0.90%) ⬆️
macOS-py3.14 91.00% <90.60%> (?)
mcp-Linux-py3.12 92.13% <87.36%> (-1.30%) ⬇️
mcp-Windows-py3.12 91.70% <84.11%> (-1.99%) ⬇️
mcp-macOS-py3.12 93.12% <91.33%> (-0.57%) ⬇️
speed-Linux-py3.12 69.03% <62.62%> (+0.39%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
PyMemoryEditor/app/cheat_table.py 52.13% <100.00%> (ø)
PyMemoryEditor/linux/functions.py 86.23% <100.00%> (ø)
PyMemoryEditor/linux/process.py 91.59% <100.00%> (+0.85%) ⬆️
PyMemoryEditor/mcp/config.py 98.18% <100.00%> (+0.06%) ⬆️
PyMemoryEditor/mcp/policy.py 98.51% <100.00%> (+0.60%) ⬆️
PyMemoryEditor/mcp/session.py 98.05% <100.00%> (+0.12%) ⬆️
PyMemoryEditor/process/_guard.py 100.00% <100.00%> (ø)
PyMemoryEditor/process/abstract.py 90.93% <100.00%> (+0.17%) ⬆️
PyMemoryEditor/process/info.py 73.33% <100.00%> (+3.33%) ⬆️
PyMemoryEditor/util/__init__.py 100.00% <ø> (ø)
... and 18 more

... and 15 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

- mcp: read /proc/<pid>/stat as bytes, so a non-UTF-8 comm no longer ends
  the ancestor walk and leaves the MCP client unprotected
- mcp: pin a shared-runtime approval to the start identity shown in the
  prompt; refuse the attach if the pid was reused while it was open
- mcp: apply the response value budget only to str/bytes scans and cut hex
  to an even length, so truncated values still parse
- mcp: echo value_type through one canonical_value_type helper
- app: drop the unused _drain_pending_writes; tests use the run() path
- app: resolve pointer-scan rows with their stored base again; only Compare
  rows whose module is not loaded are flagged and refused (rebasing every
  row onto first_module_bases picked the wrong image for duplicate names)
- mcp: re-check the policy after attaching on a remembered approval, so a
  per-instance grant cannot be used by a process that reused the pid
- backends: always sort scan regions; a MemoryRegionSnapshot is a mutable
  list and sorting an already sorted one is a linear pass
- win32: pause briefly between ERROR_BAD_LENGTH module snapshot retries
- scanning: drop the progress_callback plumbing no caller passes
- tests: avoid reference cycles through Qt objects, which let the cyclic GC
  free them off the GUI thread (worker segfault on CI)
- re-check a remembered approval off the event loop, and say whether the
  start time was unreadable or the pid was reused
- do not remember a shared-runtime approval whose start time could not be
  read before the prompt
- procinfo: reuse the Linux backend's start-time parser
- macos: the bitness probe lists modules without computing image sizes
@JeanExtreme002
JeanExtreme002 merged commit f5a240d into main Oct 12, 2026
29 checks passed
@github-actions
github-actions Bot deleted the fix/technical-review-findings branch October 12, 2026 02:10
JeanExtreme002 added a commit that referenced this pull request Oct 12, 2026
The informational app-coverage step added in #111 uploaded PyMemoryEditor/app
to Codecov, which merges every upload into one project total: ~4.5k app lines
at ~62% next to ~4.6k library and MCP lines at ~93% took the badge from 92%
to 77%. The library and MCP coverage did not change.

Drop that step and .coveragerc-app, and list the app under codecov.yml's
ignore, as the CI coverage gate (.coveragerc-lib) already leaves it out.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app GUI application changes (PyMemoryEditor/app/) docs Documentation changes (docs/) lib Library changes (PyMemoryEditor/) linux Linux backend changes (PyMemoryEditor/linux/) macOS macOS backend changes (PyMemoryEditor/macos/) mcp MCP changes (PyMemoryEditor/mcp) tests Test changes (tests/) win32 Windows backend changes (PyMemoryEditor/win32/)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants