PyMemoryEditor 3.3.0__py3-none-any.whl → 3.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. PyMemoryEditor/__init__.py +1 -1
  2. PyMemoryEditor/app/_auto_refresh_dialog.py +6 -7
  3. PyMemoryEditor/app/_widgets.py +7 -7
  4. PyMemoryEditor/app/application.py +3 -4
  5. PyMemoryEditor/app/cheat_poll_worker.py +5 -5
  6. PyMemoryEditor/app/cheat_table.py +17 -15
  7. PyMemoryEditor/app/main_window.py +11 -11
  8. PyMemoryEditor/app/memory_viewer_dialog.py +5 -5
  9. PyMemoryEditor/app/open_process_dialog.py +1 -1
  10. PyMemoryEditor/app/pointer_chain_dialog.py +2 -1
  11. PyMemoryEditor/app/pointer_scan_dialog.py +5 -5
  12. PyMemoryEditor/app/results_view.py +6 -6
  13. PyMemoryEditor/app/scanner_panel.py +1 -1
  14. PyMemoryEditor/app/value_types.py +3 -3
  15. PyMemoryEditor/linux/functions.py +29 -18
  16. PyMemoryEditor/linux/libc.py +2 -3
  17. PyMemoryEditor/linux/process.py +49 -112
  18. PyMemoryEditor/macos/functions.py +38 -25
  19. PyMemoryEditor/macos/process.py +47 -121
  20. PyMemoryEditor/mcp/config.py +12 -13
  21. PyMemoryEditor/mcp/policy.py +6 -6
  22. PyMemoryEditor/mcp/server.py +9 -9
  23. PyMemoryEditor/mcp/session.py +21 -23
  24. PyMemoryEditor/mcp/toolset.py +83 -88
  25. PyMemoryEditor/process/_guard.py +6 -5
  26. PyMemoryEditor/process/abstract.py +224 -25
  27. PyMemoryEditor/process/pointer_scan.py +2 -1
  28. PyMemoryEditor/process/region.py +10 -2
  29. PyMemoryEditor/process/scanning.py +9 -13
  30. PyMemoryEditor/util/scan.py +1 -2
  31. PyMemoryEditor/win32/enums/memory_protections.py +1 -1
  32. PyMemoryEditor/win32/functions.py +26 -15
  33. PyMemoryEditor/win32/process.py +52 -128
  34. {pymemoryeditor-3.3.0.dist-info → pymemoryeditor-3.3.2.dist-info}/METADATA +1 -1
  35. {pymemoryeditor-3.3.0.dist-info → pymemoryeditor-3.3.2.dist-info}/RECORD +38 -38
  36. {pymemoryeditor-3.3.0.dist-info → pymemoryeditor-3.3.2.dist-info}/WHEEL +0 -0
  37. {pymemoryeditor-3.3.0.dist-info → pymemoryeditor-3.3.2.dist-info}/entry_points.txt +0 -0
  38. {pymemoryeditor-3.3.0.dist-info → pymemoryeditor-3.3.2.dist-info}/licenses/LICENSE +0 -0
@@ -3,20 +3,12 @@
3
3
  import logging
4
4
  import os
5
5
  import warnings
6
- from typing import Generator, Optional, Sequence, Tuple, Type, TypeVar, Union
6
+ from typing import Generator, Optional, Pattern, Sequence, Tuple, Type, TypeVar, Union
7
7
 
8
8
  from ..enums import ScanTypesEnum
9
- from ..util.pattern import compile_pattern
10
9
  from ..process import AbstractProcess
11
- from ..process._guard import guard_generator
12
10
  from ..process.errors import ClosedProcess, ProcessIDNotExistsError
13
- from ..util import (
14
- UNSET,
15
- prepare_write,
16
- resolve_bufflength,
17
- prepare_search_target,
18
- resolve_bufflength_for_value,
19
- )
11
+ from ..util import UNSET, prepare_write, resolve_bufflength
20
12
  from ..process.module_info import ModuleInfo
21
13
  from ..process.region import MemoryRegion
22
14
  from ..process.thread_info import ThreadInfo
@@ -106,7 +98,7 @@ class LinuxProcess(AbstractProcess):
106
98
  stacklevel=2,
107
99
  )
108
100
 
109
- def __require_open(self) -> None:
101
+ def _require_open(self) -> None:
110
102
  if self.__closed:
111
103
  raise ClosedProcess()
112
104
 
@@ -121,10 +113,13 @@ class LinuxProcess(AbstractProcess):
121
113
  ``hot_path`` marks single-value reads, which callers issue in tight
122
114
  loops: they are verified only when the cheap pidfd probe is
123
115
  available. Without a pidfd the start-time read would roughly
124
- quadruple the cost of every read, so there they keep the pre-pidfd
125
- behaviour. Writes, scans and listings are always verified.
116
+ quadruple the cost of every read, so there they are not verified.
117
+ Writes are always verified, and so are scans and listings
118
+ when they are called; their generators are not re-verified between
119
+ items, so one held while the target exits and its pid is reused would
120
+ read the newcomer -- create the scan again after a long pause.
126
121
  """
127
- self.__require_open()
122
+ self._require_open()
128
123
  if hot_path and self.__pidfd is None:
129
124
  return
130
125
  if not self._target_alive():
@@ -182,17 +177,17 @@ class LinuxProcess(AbstractProcess):
182
177
  self.__require_target()
183
178
  return _detect_process_64bit(self.pid)
184
179
 
185
- def get_memory_regions(self) -> Generator[MemoryRegion, None, None]:
180
+ def _require_listing(self) -> None:
186
181
  self.__require_target()
187
- return guard_generator(get_memory_regions(self.pid), self.__require_open)
188
182
 
189
- def get_threads(self) -> Generator[ThreadInfo, None, None]:
190
- self.__require_target()
191
- return guard_generator(get_threads(self.pid), self.__require_open)
183
+ def _iter_memory_regions(self) -> Generator[MemoryRegion, None, None]:
184
+ return get_memory_regions(self.pid)
192
185
 
193
- def get_modules(self) -> Generator[ModuleInfo, None, None]:
194
- self.__require_target()
195
- return guard_generator(get_modules(self.pid), self.__require_open)
186
+ def _iter_threads(self) -> Generator[ThreadInfo, None, None]:
187
+ return get_threads(self.pid)
188
+
189
+ def _iter_modules(self) -> Generator[ModuleInfo, None, None]:
190
+ return get_modules(self.pid)
196
191
 
197
192
  def read_process_memory(
198
193
  self,
@@ -209,121 +204,63 @@ class LinuxProcess(AbstractProcess):
209
204
  self.__require_target(hot_path=True)
210
205
  return read_process_memory_into(self.pid, address, buffer)
211
206
 
212
- def search_by_addresses(
207
+ def _scan_addresses(
213
208
  self,
214
209
  pytype: Type[T],
215
- bufflength: Optional[int] = None,
216
- addresses: Sequence[int] = UNSET,
210
+ bufflength: int,
211
+ addresses: Sequence[int],
217
212
  *,
218
- raise_error: bool = False,
219
- memory_regions: Optional[Sequence[MemoryRegion]] = None,
213
+ raise_error: bool,
214
+ memory_regions: Optional[Sequence[MemoryRegion]],
220
215
  ) -> Generator[Tuple[int, Optional[T]], None, None]:
221
- self.__require_target()
222
- if addresses is UNSET:
223
- raise TypeError("addresses is required.")
224
- scan = search_values_by_addresses(
216
+ return search_values_by_addresses(
225
217
  self.pid,
226
218
  pytype,
227
- resolve_bufflength(pytype, bufflength),
219
+ bufflength,
228
220
  addresses,
229
221
  memory_regions=memory_regions,
230
222
  raise_error=raise_error,
231
223
  )
232
- return guard_generator(scan, self.__require_open)
233
224
 
234
- def search_by_value(
225
+ def _scan_values(
235
226
  self,
236
- pytype: Type[T],
237
- bufflength: Optional[int] = None,
238
- value: Union[bool, int, float, str, bytes] = UNSET,
239
- scan_type: ScanTypesEnum = ScanTypesEnum.EXACT_VALUE,
227
+ pytype: Type,
228
+ bufflength: int,
229
+ target_value_bytes: Union[bytes, Tuple[bytes, ...]],
230
+ scan_type: ScanTypesEnum,
240
231
  *,
241
- progress_information: bool = False,
242
- writeable_only: bool = False,
243
- memory_regions: Optional[Sequence[MemoryRegion]] = None,
244
- aligned: bool = True,
232
+ progress_information: bool,
233
+ writeable_only: bool,
234
+ memory_regions: Optional[Sequence[MemoryRegion]],
235
+ aligned: bool,
245
236
  ) -> Generator[Union[int, Tuple[int, dict]], None, None]:
246
- self.__require_target()
247
-
248
- if scan_type in [ScanTypesEnum.VALUE_BETWEEN, ScanTypesEnum.NOT_VALUE_BETWEEN]:
249
- raise ValueError(
250
- "Use the method search_by_value_between(...) to search within a range of values."
251
- )
252
-
253
- width = resolve_bufflength_for_value(pytype, bufflength, value)
254
- # Validate now: the scan is a generator, so its own checks would
255
- # only raise on the first next(), far from this call.
256
- prepare_search_target(pytype, width, value)
257
- scan = search_addresses_by_value(
237
+ return search_addresses_by_value(
258
238
  self.pid,
259
239
  pytype,
260
- width,
261
- value,
262
- scan_type,
263
- progress_information,
264
- writeable_only,
240
+ bufflength,
241
+ value=None, # given already encoded, as target_value_bytes
242
+ scan_type=scan_type,
243
+ progress_information=progress_information,
244
+ writeable_only=writeable_only,
265
245
  memory_regions=memory_regions,
266
246
  aligned=aligned,
247
+ target_value_bytes=target_value_bytes,
267
248
  )
268
- return guard_generator(scan, self.__require_open)
269
249
 
270
- def search_by_pattern(
250
+ def _scan_pattern(
271
251
  self,
272
- pattern,
252
+ compiled_pattern: Tuple[Pattern[bytes], int],
273
253
  *,
274
- byte_length: int = 0,
275
- progress_information: bool = False,
276
- memory_regions: Optional[Sequence[MemoryRegion]] = None,
254
+ progress_information: bool,
255
+ memory_regions: Optional[Sequence[MemoryRegion]],
277
256
  ) -> Generator[Union[int, Tuple[int, dict]], None, None]:
278
- self.__require_target()
279
- # Compile now so a malformed pattern raises at the call, not on the
280
- # first next() of the returned generator.
281
- compile_pattern(pattern, byte_length=byte_length)
282
- scan = search_addresses_by_pattern(
257
+ return search_addresses_by_pattern(
283
258
  self.pid,
284
- pattern,
285
- byte_length=byte_length,
259
+ compiled_pattern[0],
286
260
  progress_information=progress_information,
287
261
  memory_regions=memory_regions,
262
+ compiled_pattern=compiled_pattern,
288
263
  )
289
- return guard_generator(scan, self.__require_open)
290
-
291
- def search_by_value_between(
292
- self,
293
- pytype: Type[T],
294
- bufflength: Optional[int] = None,
295
- start: Union[bool, int, float, str, bytes] = UNSET,
296
- end: Union[bool, int, float, str, bytes] = UNSET,
297
- *,
298
- not_between: bool = False,
299
- progress_information: bool = False,
300
- writeable_only: bool = False,
301
- memory_regions: Optional[Sequence[MemoryRegion]] = None,
302
- aligned: bool = True,
303
- ) -> Generator[Union[int, Tuple[int, dict]], None, None]:
304
- self.__require_target()
305
-
306
- scan_type = (
307
- ScanTypesEnum.NOT_VALUE_BETWEEN
308
- if not_between
309
- else ScanTypesEnum.VALUE_BETWEEN
310
- )
311
- width = resolve_bufflength_for_value(pytype, bufflength, start, end)
312
- # Validate now: the scan is a generator, so its own checks would
313
- # only raise on the first next(), far from this call.
314
- prepare_search_target(pytype, width, (start, end))
315
- scan = search_addresses_by_value(
316
- self.pid,
317
- pytype,
318
- width,
319
- (start, end),
320
- scan_type,
321
- progress_information,
322
- writeable_only,
323
- memory_regions=memory_regions,
324
- aligned=aligned,
325
- )
326
- return guard_generator(scan, self.__require_open)
327
264
 
328
265
  def write_process_memory(
329
266
  self,
@@ -338,7 +275,7 @@ class LinuxProcess(AbstractProcess):
338
275
  return value
339
276
 
340
277
  def allocate_memory(self, size: int, *, permission=None) -> int:
341
- self.__require_open()
278
+ self._require_open()
342
279
  raise NotImplementedError(
343
280
  "allocate_memory is not supported on Linux: there is no syscall to "
344
281
  "allocate memory in another process's address space (mmap only "
@@ -348,7 +285,7 @@ class LinuxProcess(AbstractProcess):
348
285
  )
349
286
 
350
287
  def free_memory(self, address: int, size: int = 0) -> bool:
351
- self.__require_open()
288
+ self._require_open()
352
289
  raise NotImplementedError(
353
290
  "free_memory is not supported on Linux (see allocate_memory): "
354
291
  "releasing memory in another process would require a ptrace-based "
@@ -10,12 +10,13 @@ import logging
10
10
  import os
11
11
  import warnings
12
12
  from contextlib import contextmanager
13
- from typing import Generator, Iterator, List, Optional, Sequence, Tuple, Type, TypeVar, Union
13
+ from typing import Generator, Iterator, List, Optional, Pattern, Sequence, Tuple, Type, TypeVar, Union, cast
14
14
 
15
15
  from ..enums import ScanTypesEnum
16
16
  from ..process.errors import ProcessIDNotExistsError
17
17
  from ..process.module_info import ModuleInfo
18
18
  from ..process.region import (
19
+ ADDRESS_SPACE_END,
19
20
  default_address_filter,
20
21
  default_scan_filter,
21
22
  MemoryRegion,
@@ -96,11 +97,10 @@ _REGION_SKIP_BUMP = 0x1000
96
97
  # the whole 64-bit address space.
97
98
  _REGION_MAX_CONSECUTIVE_FAILURES = 64
98
99
 
99
- # One past the highest address a `mach_vm_address_t` (c_uint64) can hold. The
100
- # walk advances its cursor in place, and ctypes wraps silently on overflow: a
101
- # region reaching the top of the address space would send the cursor back to 0
102
- # and restart the walk from the bottom.
103
- _ADDRESS_SPACE_END = 1 << 64
100
+ # The region walk advances a `mach_vm_address_t` (c_uint64) cursor in place,
101
+ # and ctypes wraps silently on overflow: a region reaching the top of the
102
+ # address space would send the cursor back to 0 and restart the walk from the
103
+ # bottom. It stops at ADDRESS_SPACE_END instead.
104
104
 
105
105
  # mach_vm_write's data_count parameter is `mach_msg_type_number_t` — a 32-bit
106
106
  # unsigned int per the Mach interface. A write larger than UINT32_MAX would
@@ -182,7 +182,8 @@ def release_task(task: int) -> bool:
182
182
  if kr != KERN_SUCCESS:
183
183
  # A failed deallocate leaks the send right for the lifetime of the
184
184
  # process. It shouldn't happen for a port we own, but swallowing it
185
- # silently hid real leaks — log it so they're at least visible.
185
+ # silently would hide real leaks — log it so they're at least
186
+ # visible.
186
187
  _logger.warning(
187
188
  "mach_port_deallocate failed for task port %d: %s (kr=%d)",
188
189
  task,
@@ -287,8 +288,8 @@ def get_memory_regions(task: int, pid: int = 0) -> Generator[MemoryRegion, None,
287
288
  ``mach_vm_region`` returning :data:`KERN_INVALID_ADDRESS` is the documented
288
289
  way the kernel says "no more regions past this address" — the natural end
289
290
  of the enumeration. Any *other* non-success code (``KERN_FAILURE``,
290
- ``KERN_NO_ACCESS`` on a guard page, etc.) used to terminate enumeration
291
- too, silently truncating the region list. Now those are logged and the
291
+ ``KERN_NO_ACCESS`` on a guard page, etc.) does not end the enumeration,
292
+ which would silently truncate the region list: it is logged and the
292
293
  cursor is bumped one page so the walk keeps going.
293
294
 
294
295
  That skip is bounded: :data:`MACH_SEND_INVALID_DEST` (the task behind the
@@ -331,7 +332,7 @@ def get_memory_regions(task: int, pid: int = 0) -> Generator[MemoryRegion, None,
331
332
  "advancing one page",
332
333
  address.value, kr, mach_error_message(kr),
333
334
  )
334
- if address.value + _REGION_SKIP_BUMP >= _ADDRESS_SPACE_END:
335
+ if address.value + _REGION_SKIP_BUMP >= ADDRESS_SPACE_END:
335
336
  return
336
337
  address.value += _REGION_SKIP_BUMP
337
338
  continue
@@ -358,7 +359,7 @@ def get_memory_regions(task: int, pid: int = 0) -> Generator[MemoryRegion, None,
358
359
 
359
360
  if size.value == 0:
360
361
  # Defensive: a successful query should never report an empty
361
- # region. Stopping here (the old behaviour) would drop every
362
+ # region. Stopping here would drop every
362
363
  # region above it, so step one page past it like the Windows
363
364
  # walk does — counted as a failure, so a kernel answering 0
364
365
  # forever can't turn this into a crawl across the address space.
@@ -375,7 +376,7 @@ def get_memory_regions(task: int, pid: int = 0) -> Generator[MemoryRegion, None,
375
376
  consecutive_failures = 0
376
377
  step = size.value
377
378
 
378
- if address.value + step >= _ADDRESS_SPACE_END:
379
+ if address.value + step >= ADDRESS_SPACE_END:
379
380
  return # the last region reaches the top of the address space
380
381
  address.value += step
381
382
 
@@ -392,7 +393,7 @@ def get_memory_regions(task: int, pid: int = 0) -> Generator[MemoryRegion, None,
392
393
  # KERN_MEMORY_FAILURE beside it (the pair is spelled out in types.py). It is
393
394
  # what file-backed, read-only mappings (code segments, dylibs, the dyld shared
394
395
  # cache) return, so any scan that walks them — every pattern scan, and any value
395
- # scan with "writable regions only" turned off — hits it routinely, and used to
396
+ # scan with "writable regions only" turned off — hits it routinely and must not
396
397
  # abort on the first one.
397
398
  _PAGE_GONE_KRS = (
398
399
  KERN_INVALID_ADDRESS,
@@ -416,10 +417,10 @@ _PAGE_GONE_KRS = (
416
417
  # silently turn "permission denied" into the same `(address, None)` the caller
417
418
  # gets for an address in a gap.
418
419
  #
419
- # Honest limits of what is known here. This was added because a plain
420
- # search_by_value over the current process died with kr=2 on GitHub's
421
- # virtualized macOS runners, on a 22 MB region that reported VM_PROT_READ and
422
- # Shared=False. The mechanism was not established. It does *not* reproduce
420
+ # Honest limits of what is known here. A plain search_by_value over the
421
+ # current process gets kr=2 on GitHub's virtualized macOS runners, on a 22 MB
422
+ # region that reports VM_PROT_READ and Shared=False. The mechanism is not
423
+ # established. It does *not* reproduce
423
424
  # locally: probing all 135 scannable regions of a live process yields only kr=1
424
425
  # and kr=10, and a page deliberately re-protected to PROT_NONE (or never
425
426
  # mapped) answers KERN_INVALID_ADDRESS, not kr=2 — so the usual
@@ -442,8 +443,8 @@ class MachPartialReadError(MachReadError):
442
443
  because the read straddled a freed or guarded page) and the caller's
443
444
  buffer is part real-bytes, part zero-initialized.
444
445
 
445
- The previous behavior silently accepted the short result, which let
446
- downstream code decode garbage as valid memory. Mirrors the Win32
446
+ Accepting the short result would let downstream code decode garbage as
447
+ valid memory, so it raises instead. Mirrors the Win32
447
448
  partial-read check on ``ReadProcessMemory``. Scan loops classify this
448
449
  as transient so the chunk is skipped instead of aborting.
449
450
  """
@@ -767,9 +768,9 @@ def read_process_memory(
767
768
  _mach_read(task, address, ctypes.addressof(data), bufflength)
768
769
 
769
770
  if pytype is str:
770
- return bytes(data).decode("utf-8", errors="replace")
771
+ return cast(T, bytes(data).decode("utf-8", errors="replace"))
771
772
  elif pytype is bytes:
772
- return bytes(data)
773
+ return cast(T, bytes(data))
773
774
  else:
774
775
  # Narrow widths pad with zeroes, which reads a signed value as
775
776
  # unsigned -- and a scan for the same bytes is signed.
@@ -866,7 +867,7 @@ def search_addresses_by_value(
866
867
  task: int,
867
868
  pytype: Type[T],
868
869
  bufflength: int,
869
- value: Union[bool, int, float, str, bytes, tuple],
870
+ value: Optional[Union[bool, int, float, str, bytes, tuple]],
870
871
  scan_type: ScanTypesEnum = ScanTypesEnum.EXACT_VALUE,
871
872
  progress_information: bool = False,
872
873
  writeable_only: bool = False,
@@ -874,6 +875,7 @@ def search_addresses_by_value(
874
875
  memory_regions: Optional[Sequence[MemoryRegion]] = None,
875
876
  pid: int = 0,
876
877
  aligned: bool = True,
878
+ target_value_bytes: Optional[Union[bytes, Tuple[bytes, ...]]] = None,
877
879
  ) -> Generator[Union[int, Tuple[int, dict]], None, None]:
878
880
  """
879
881
  Walk every readable, non-shared region of the task and yield addresses
@@ -881,10 +883,14 @@ def search_addresses_by_value(
881
883
  (including the dyld shared cache) are skipped via ``default_scan_filter``.
882
884
 
883
885
  Passing a `memory_regions` snapshot skips region enumeration.
886
+ ``target_value_bytes`` is ``value`` already encoded (see
887
+ :func:`~PyMemoryEditor.util.prepare_search_target`); when it is given,
888
+ ``value`` is not used and may be ``None``.
884
889
  """
885
890
  _validate_pytype(pytype)
886
891
 
887
- target_value_bytes = values_to_bytes(pytype, bufflength, value)
892
+ if target_value_bytes is None:
893
+ target_value_bytes = values_to_bytes(pytype, bufflength, value)
888
894
 
889
895
  source_regions = (
890
896
  memory_regions if memory_regions is not None else get_memory_regions(task, pid)
@@ -1385,13 +1391,20 @@ def search_addresses_by_pattern(
1385
1391
  byte_length: int = 0,
1386
1392
  progress_information: bool = False,
1387
1393
  memory_regions: Optional[Sequence[MemoryRegion]] = None,
1394
+ compiled_pattern: Optional[Tuple[Pattern[bytes], int]] = None,
1388
1395
  pid: int = 0,
1389
1396
  ) -> Generator[Union[int, Tuple[int, dict]], None, None]:
1390
1397
  """
1391
1398
  AOB scan against every readable, non-shared region of the target task. See
1392
- :meth:`AbstractProcess.search_by_pattern`.
1399
+ :meth:`AbstractProcess.search_by_pattern`. ``compiled_pattern`` is what
1400
+ :func:`~PyMemoryEditor.util.pattern.compile_pattern` returned for
1401
+ ``pattern``, when the caller already compiled it.
1393
1402
  """
1394
- compiled, length = compile_pattern(pattern, byte_length=byte_length)
1403
+ compiled, length = (
1404
+ compiled_pattern
1405
+ if compiled_pattern is not None
1406
+ else compile_pattern(pattern, byte_length=byte_length)
1407
+ )
1395
1408
 
1396
1409
  source_regions = (
1397
1410
  memory_regions if memory_regions is not None else get_memory_regions(task, pid)