@memoryprocess/bindings 0.0.1 → 0.0.3-beta.2

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.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @memoryprocess/bindings — the native N-API layer.
2
+ * @memoryprocess/bindings -- the native N-API layer.
3
3
  * Exposes the raw `.node` module plus its TypeScript interface.
4
4
  * Most consumers want `@memoryprocess/core` (the Process class) instead.
5
5
  */
Binary file
package/dist/native.d.ts CHANGED
@@ -77,11 +77,11 @@ interface Native {
77
77
  * RELOCATE position-relative operands (rel32 branch/call immediates,
78
78
  * RIP-relative disp32) so the stolen bytes are correct when executed from
79
79
  * `stubAddr` (must be within rel32 range of `address`). External rel8/16
80
- * branches (incl. LOOP/JECXZ) can't be widened — refused `HOOK_RELOCATE`,
80
+ * branches (incl. LOOP/JECXZ) can't be widened -- refused `HOOK_RELOCATE`,
81
81
  * like MinHook; rel8/16 internal (target inside the stolen window) and
82
82
  * all non-relative instructions copy verbatim. Without `stubAddr` any
83
83
  * position-relative instruction is refused (inspection mode).
84
- * Throws `HOOK_STEAL`/`HOOK_RELOCATE` — returns the relocated bytes.
84
+ * Throws `HOOK_STEAL`/`HOOK_RELOCATE` -- returns the relocated bytes.
85
85
  */
86
86
  hookSteal(handle: number, address: number, minBytesOrOpts?: number | {
87
87
  minBytes?: number;
@@ -92,7 +92,7 @@ interface Native {
92
92
  original: Buffer;
93
93
  };
94
94
  /**
95
- * Per-region VirtualProtectEx→write→restore across the whole write span —
95
+ * Per-region VirtualProtectEx->write->restore across the whole write span --
96
96
  * records each region's real old protection before writing, restores each
97
97
  * individually. `restored=false` means part of the span is left weakened.
98
98
  */
@@ -101,7 +101,7 @@ interface Native {
101
101
  restored: boolean;
102
102
  protected: boolean;
103
103
  };
104
- /** Current thread id (TID) — used to refuse self-suspend. */
104
+ /** Current thread id (TID) -- used to refuse self-suspend. */
105
105
  currentThreadId(): number;
106
106
  suspendProcess(pid: number): boolean;
107
107
  resumeProcess(pid: number): boolean;
@@ -1,4 +1,4 @@
1
- MinHook - The Minimalistic API Hooking Library for x64/x86
1
+ MinHook - The Minimalistic API Hooking Library for x64/x86
2
2
  Copyright (C) 2009-2017 Tsuda Kageyu.
3
3
  All rights reserved.
4
4
 
@@ -1,4 +1,4 @@
1
- /*
1
+ /*
2
2
  * Hacker Disassembler Engine 64 C
3
3
  * Copyright (c) 2008-2009, Vyacheslav Patkov.
4
4
  * All rights reserved.
@@ -1,4 +1,4 @@
1
- /*
1
+ /*
2
2
  * Hacker Disassembler Engine 64
3
3
  * Copyright (c) 2008-2009, Vyacheslav Patkov.
4
4
  * All rights reserved.
@@ -1,4 +1,4 @@
1
- /*
1
+ /*
2
2
  * MinHook - The Minimalistic API Hooking Library for x64/x86
3
3
  * Copyright (C) 2009-2017 Tsuda Kageyu. All rights reserved.
4
4
  *
@@ -1,4 +1,4 @@
1
- /*
1
+ /*
2
2
  * Hacker Disassembler Engine 64 C
3
3
  * Copyright (c) 2008-2009, Vyacheslav Patkov.
4
4
  * All rights reserved.
package/native/module.cc CHANGED
@@ -12,12 +12,12 @@ extern "C" {
12
12
 
13
13
  namespace mp {
14
14
 
15
- // Max bytes a single read call may materialize — guards against negative /
15
+ // Max bytes a single read call may materialize - guards against negative /
16
16
  // non-finite / absurd `size` values blowing up std::vector across the N-API
17
17
  // boundary (a length_error crossing N-API aborts the process).
18
18
  static const double MAX_READ_BYTES = 512.0 * 1024.0 * 1024.0;
19
19
 
20
- // NOTE: handles and addresses cross N-API as doubles — above 2^53 they would
20
+ // NOTE: handles and addresses cross N-API as doubles - above 2^53 they would
21
21
  // silently round. Harmless today (canonical user-space VA < 2^48, real HANDLE
22
22
  // values are small) but don't feed pointers >2^53 through these bindings.
23
23
 
@@ -165,8 +165,8 @@ Napi::Value WriteBufferSync(const Napi::CallbackInfo& info) {
165
165
  // ---------- v2 Tier-1/2 ----------
166
166
 
167
167
  // allocMemory(handle, size, protection[, nearAddress]) -> bigint
168
- // `nearAddress` is passed to VirtualAllocEx as lpAddress — a hint used to land
169
- // trampoline stubs within rel32 range (±2GB) of a hook site.
168
+ // `nearAddress` is passed to VirtualAllocEx as lpAddress - a hint used to land
169
+ // trampoline stubs within rel32 range (+/-2GB) of a hook site.
170
170
  Napi::Value AllocMemorySync(const Napi::CallbackInfo& info) {
171
171
  Napi::Env env = info.Env();
172
172
  if (info.Length() < 3) { Napi::TypeError::New(env, "expected (handle,size,protection[,nearAddress])").ThrowAsJavaScriptException(); return env.Null(); }
@@ -234,7 +234,7 @@ Napi::Value SuspendThreadSync(const Napi::CallbackInfo& info) {
234
234
  if (info.Length() < 1 || !info[0].IsNumber()) { Napi::TypeError::New(env, "expected tid").ThrowAsJavaScriptException(); return env.Null(); }
235
235
  DWORD tid = info[0].As<Napi::Number>().Uint32Value();
236
236
  // QA4-C3: suspending OUR OWN thread from a sync binding freezes the JS
237
- // engine mid-call — it can never reach resumeThread (unrecoverable
237
+ // engine mid-call - it can never reach resumeThread (unrecoverable
238
238
  // deadlock). Refuse silently (false) so suspendAll() on the caller's own
239
239
  // pid skips the calling thread instead of hanging.
240
240
  if (tid == GetCurrentThreadId()) return Napi::Boolean::New(env, false);
@@ -384,13 +384,13 @@ Napi::Value FlushInstructionsSync(const Napi::CallbackInfo& info) {
384
384
 
385
385
  // writeProtectedMemory(handle, address, buffer[, protection]) -> boolean
386
386
  // QA4-M1: the old TS flow did protect(addr,len)->oldProt(first page only)->
387
- // write->protect(addr,len,oldProt) — a write spanning a protection boundary
387
+ // write->protect(addr,len,oldProt) - a write spanning a protection boundary
388
388
  // restored EVERY page in the span to the first page's old protection,
389
389
  // silently changing protections on the second region. This native
390
390
  // implementation walks VirtualQueryEx over the write span, records each
391
391
  // region's real old protection, unprotects the whole span, writes, then
392
392
  // restores each region individually. Single-region writes keep identical
393
- // behavior. Restore failures are reported via the `restored` flag — a
393
+ // behavior. Restore failures are reported via the `restored` flag - a
394
394
  // restored=false result means part of the span is left at `protection`.
395
395
  Napi::Value WriteProtectedMemorySync(const Napi::CallbackInfo& info) {
396
396
  Napi::Env env = info.Env();
@@ -410,7 +410,7 @@ Napi::Value WriteProtectedMemorySync(const Napi::CallbackInfo& info) {
410
410
  if (info.Length() > 3 && info[3].IsNumber())
411
411
  newProt = info[3].As<Napi::Number>().Uint32Value();
412
412
  if (buf.Length() == 0) {
413
- // Nothing to do — return the same result shape as a real write.
413
+ // Nothing to do - return the same result shape as a real write.
414
414
  auto o = Napi::Object::New(env);
415
415
  o.Set("ok", Napi::Boolean::New(env, true));
416
416
  o.Set("restored", Napi::Boolean::New(env, true));
@@ -419,7 +419,7 @@ Napi::Value WriteProtectedMemorySync(const Napi::CallbackInfo& info) {
419
419
  }
420
420
 
421
421
  // QA4-M1: record per-region old protection across the whole write span
422
- // BEFORE unprotecting — VirtualProtectEx only reports the first page's
422
+ // BEFORE unprotecting - VirtualProtectEx only reports the first page's
423
423
  // old value, so the query walk is the only correct source.
424
424
  struct RegionSnap { std::uintptr_t base; std::uintptr_t end; DWORD oldProt; };
425
425
  std::vector<RegionSnap> regions;
@@ -445,13 +445,13 @@ Napi::Value WriteProtectedMemorySync(const Napi::CallbackInfo& info) {
445
445
  }
446
446
 
447
447
  // Unprotect each region span (VirtualProtectEx clamps to whole pages, which
448
- // is fine — only pages intersecting [addr,end) are touched).
448
+ // is fine - only pages intersecting [addr,end) are touched).
449
449
  bool allProtected = true;
450
450
  for (const auto& r : regions) {
451
451
  DWORD old = 0;
452
452
  if (!VirtualProtectEx(h, reinterpret_cast<LPVOID>(r.base), r.end - r.base,
453
453
  newProt, &old)) {
454
- allProtected = false; // continue — best effort
454
+ allProtected = false; // continue - best effort
455
455
  }
456
456
  }
457
457
 
@@ -487,9 +487,9 @@ static void ThrowInject(Napi::Env env, const char* what, DWORD code) {
487
487
 
488
488
  // injectDll(pid, dllPath) -> boolean
489
489
  // QA4-M2: dllPath must be ABSOLUTE. A relative name resolves against the
490
- // TARGET's cwd + DLL search order — a DLL-planting primitive when that cwd is
490
+ // TARGET's cwd + DLL search order - a DLL-planting primitive when that cwd is
491
491
  // attacker-writable. Reject anything that isn't `X:\...` or a `\\` UNC path.
492
- // QA4-M2 also: the path is written UTF-16LE and LoadLibraryW is used — the
492
+ // QA4-M2 also: the path is written UTF-16LE and LoadLibraryW is used - the
493
493
  // old LoadLibraryA path mangled non-ANSI filenames per the target's codepage.
494
494
  static bool IsAbsolutePath(const std::string& p) {
495
495
  if (p.size() >= 3 && ((p[0] >= 'A' && p[0] <= 'Z') || (p[0] >= 'a' && p[0] <= 'z')) &&
@@ -516,7 +516,7 @@ Napi::Value InjectDllSync(const Napi::CallbackInfo& info) {
516
516
  ThrowInject(env, "dllPath exceeds maximum NT path length (32767)", ERROR_FILENAME_EXCED_RANGE);
517
517
  return env.Null();
518
518
  }
519
- // Optional 3rd arg: remote-thread wait timeout (bounded — a negative or
519
+ // Optional 3rd arg: remote-thread wait timeout (bounded - a negative or
520
520
  // huge value would wrap DWORD into an ~infinite sync wait, QA4-H1).
521
521
  DWORD waitMs = 10000;
522
522
  if (info.Length() > 2 && info[2].IsNumber()) {
@@ -541,7 +541,7 @@ Napi::Value InjectDllSync(const Napi::CallbackInfo& info) {
541
541
  HANDLE h = OpenProcess(access, FALSE, pid);
542
542
  if (!h) { ThrowInject(env, "OpenProcess failed", GetLastError()); return env.Null(); }
543
543
 
544
- // WoW64 targets load a 32-bit kernel32 at a different base — LoadLibraryW
544
+ // WoW64 targets load a 32-bit kernel32 at a different base - LoadLibraryW
545
545
  // resolved in *our* (x64) process would be the wrong address. Refuse rather
546
546
  // than silently corrupting the target.
547
547
  {
@@ -587,10 +587,10 @@ Napi::Value InjectDllSync(const Napi::CallbackInfo& info) {
587
587
  if (wr != WAIT_OBJECT_0) {
588
588
  // QA4-C1: do NOT TerminateThread here. TerminateThread is asynchronous
589
589
  // and killing a thread inside LoadLibraryW can leave the target's
590
- // loader lock permanently held — every subsequent LoadLibrary /
590
+ // loader lock permanently held - every subsequent LoadLibrary /
591
591
  // thread-creation in the victim deadlocks. Instead we close our handle
592
592
  // and deliberately LEAK the small path buffer so a late-finishing
593
- // thread never reads freed memory. Bounded leak ≤64KB; a wedged target
593
+ // thread never reads freed memory. Bounded leak <=64KB; a wedged target
594
594
  // is worse.
595
595
  CloseHandle(t);
596
596
  ThrowInject(env, "LoadLibraryW remote thread timed out (leaked path buffer; target not terminated to avoid loader-lock deadlock)",
@@ -628,7 +628,7 @@ Napi::Value EjectDllSync(const Napi::CallbackInfo& info) {
628
628
  HANDLE h = OpenProcess(access, FALSE, pid);
629
629
  if (!h) { ThrowInject(env, "OpenProcess failed", GetLastError()); return env.Null(); }
630
630
 
631
- // QA4-C4: same WoW64 guard as injectDll — our 64-bit FreeLibrary address is
631
+ // QA4-C4: same WoW64 guard as injectDll - our 64-bit FreeLibrary address is
632
632
  // meaningless inside a 32-bit target's address space; the remote thread
633
633
  // would execute a wild pointer.
634
634
  {
@@ -652,7 +652,7 @@ Napi::Value EjectDllSync(const Napi::CallbackInfo& info) {
652
652
  if (!t) { ThrowInject(env, "CreateRemoteThread failed", GetLastError()); break; }
653
653
  DWORD wr = WaitForSingleObject(t, 10000);
654
654
  if (wr != WAIT_OBJECT_0) {
655
- // QA4-C1: never TerminateThread inside FreeLibrary — the remote thread
655
+ // QA4-C1: never TerminateThread inside FreeLibrary - the remote thread
656
656
  // can hold the loader lock; killing it wedges the target permanently.
657
657
  // Just close our handle and report the timeout.
658
658
  CloseHandle(t);
@@ -660,7 +660,7 @@ Napi::Value EjectDllSync(const Napi::CallbackInfo& info) {
660
660
  wr == WAIT_FAILED ? GetLastError() : ERROR_TIMEOUT);
661
661
  break;
662
662
  }
663
- // QA4-C4: FreeLibrary returns BOOL as the thread exit code — verify it.
663
+ // QA4-C4: FreeLibrary returns BOOL as the thread exit code - verify it.
664
664
  // Without this check, ejecting kernel32/main.exe/garbage bases reported
665
665
  // success while nothing (or worse, the wrong thing) happened.
666
666
  DWORD exitCode = 0;
@@ -805,7 +805,7 @@ Napi::Value CallRemoteSync(const Napi::CallbackInfo& info) {
805
805
  }
806
806
  DWORD timeout = 10000;
807
807
  if (info.Length() > 3 && info[3].IsNumber()) {
808
- // QA4-H1: Uint32Value() on -1 wraps to 0xFFFFFFFF = INFINITE — a sync
808
+ // QA4-H1: Uint32Value() on -1 wraps to 0xFFFFFFFF = INFINITE - a sync
809
809
  // binding that never returns bricks the whole JS process. Clamp to a
810
810
  // bounded range; negative/non-finite values are a caller bug.
811
811
  double t = info[3].As<Napi::Number>().DoubleValue();
@@ -825,13 +825,13 @@ Napi::Value CallRemoteSync(const Napi::CallbackInfo& info) {
825
825
  if (!t) { CloseHandle(h); return env.Null(); }
826
826
  DWORD wr = WaitForSingleObject(t, timeout);
827
827
  DWORD code = 0;
828
- // QA4-C1: TerminateThread is asynchronous — it only *requests* termination.
828
+ // QA4-C1: TerminateThread is asynchronous - it only *requests* termination.
829
829
  // Returning immediately after TerminateThread lets the caller (execShellcode
830
830
  // finally-block, injectDll) free memory the zombie thread may still be
831
831
  // executing in. Wait for the thread object itself to be signaled (max 2s)
832
832
  // so a timeout-return really means the thread is dead. NOTE: terminating a
833
833
  // thread that was mid-LoadLibrary can still leave the target's loader lock
834
- // held — an inherent TerminateThread hazard; callers needing guaranteed
834
+ // held - an inherent TerminateThread hazard; callers needing guaranteed
835
835
  // non-wedging behavior should prefer a longer timeout over termination.
836
836
  if (wr != WAIT_OBJECT_0) {
837
837
  TerminateThread(t, 0);
@@ -862,7 +862,7 @@ Napi::Value DisasmInstructionsSync(const Napi::CallbackInfo& info) {
862
862
  auto addr = static_cast<std::uintptr_t>(info[1].As<Napi::Number>().DoubleValue());
863
863
  auto count = static_cast<std::size_t>(info[2].As<Napi::Number>().DoubleValue());
864
864
  if (count == 0) return Napi::Array::New(env, 0);
865
- if (count > 256) count = 256; // 256*15 ≈ 4KB window; also prevents count*16 wraparound
865
+ if (count > 256) count = 256; // 256*15 ~ 4KB window; also prevents count*16 wraparound
866
866
 
867
867
  std::size_t readSize = count * 16; // max x64 instruction length is 15
868
868
  if (readSize > 4096) readSize = 4096;
@@ -882,7 +882,7 @@ Napi::Value DisasmInstructionsSync(const Napi::CallbackInfo& info) {
882
882
  std::size_t off = 0, n = 0;
883
883
  while (off < buf.size() && n < count) {
884
884
  // hde64_disasm takes no length and may read up to 15 bytes for one
885
- // instruction — feeding it buf.data()+off directly walks past the end of
885
+ // instruction - feeding it buf.data()+off directly walks past the end of
886
886
  // the allocation on a truncated tail. Decode from a 16-byte padded stack
887
887
  // scratch instead; the trailing zeros make a truncated insn fail cleanly.
888
888
  std::uint8_t scratch[16] = {};
@@ -915,18 +915,18 @@ Napi::Value DisasmInstructionsSync(const Napi::CallbackInfo& info) {
915
915
  // MinHook-style relocation: steals whole instructions until >= minBytes are
916
916
  // covered, then rewrites position-relative operands so the stolen bytes are
917
917
  // correct when executed from `stubAddr` (which must be within rel32 range of
918
- // `address` — the caller's #allocNear guarantees this):
919
- // - rel32 branch/call (E8/E9/0F8x, xbegin): rel32 rewritten in place, OR
918
+ // `address` - the caller's #allocNear guarantees this):
919
+ // - rel32 branch/call (E8/E9/0F8x, xbegin): rel32 rewritten in place, OR
920
920
  // copied verbatim when the target is INSIDE the stolen window (MinHook's
921
- // "internal jump" rule — distances are preserved by the copy).
922
- // - RIP-relative ModRM (mod=00 rm=101): disp32 rewritten in place so it
921
+ // "internal jump" rule - distances are preserved by the copy).
922
+ // - RIP-relative ModRM (mod=00 rm=101): disp32 rewritten in place so it
923
923
  // still reaches the original absolute target.
924
- // - rel8/rel16 branches incl. LOOP/JECXZ (E0-E3): verbatim if internal —
925
- // the operand is position-independent within the window — otherwise
924
+ // - rel8/rel16 branches incl. LOOP/JECXZ (E0-E3): verbatim if internal -
925
+ // the operand is position-independent within the window - otherwise
926
926
  // REFUSED (rel8 can't be widened to a same-length rewrite; MinHook
927
927
  // refuses LOOP/JECXZ-external too).
928
928
  // Without `stubAddr` every position-relative instruction is refused
929
- // (HOOK_RELOCATE) — the old verify-only behavior for raw inspection calls.
929
+ // (HOOK_RELOCATE) - the old verify-only behavior for raw inspection calls.
930
930
  // Throws HOOK_STEAL when <minBytes of decodable instructions exist, or
931
931
  // HOOK_RELOCATE naming the offending opcode/offset. Returns the stolen bytes
932
932
  // so the caller doesn't need a second remote read.
@@ -942,9 +942,9 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
942
942
  std::size_t minBytes = 5;
943
943
  std::uintptr_t stub = 0;
944
944
  // Accepted arg shapes:
945
- // hookSteal(h, a, 5) — minBytes number
946
- // hookSteal(h, a, 5, stub) — minBytes + stubAddr (number|bigint)
947
- // hookSteal(h, a, {minBytes, stubAddr}) — options object
945
+ // hookSteal(h, a, 5) - minBytes number
946
+ // hookSteal(h, a, 5, stub) - minBytes + stubAddr (number|bigint)
947
+ // hookSteal(h, a, {minBytes, stubAddr}) - options object
948
948
  if (info.Length() > 2) {
949
949
  if (info[2].IsNumber()) {
950
950
  double m = info[2].As<Napi::Number>().DoubleValue();
@@ -967,7 +967,7 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
967
967
  }
968
968
  }
969
969
  }
970
- // An instruction beginning <15 bytes before minBytes can overshoot past it —
970
+ // An instruction beginning <15 bytes before minBytes can overshoot past it -
971
971
  // read minBytes+15 so no valid tail instruction truncates.
972
972
  std::vector<std::uint8_t> buf;
973
973
  try {
@@ -986,7 +986,7 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
986
986
  err.ThrowAsJavaScriptException();
987
987
  };
988
988
 
989
- // Pass 1: decode — collect each instruction's decode state for relocation.
989
+ // Pass 1: decode - collect each instruction's decode state for relocation.
990
990
  struct StolenInsn {
991
991
  std::size_t off; // offset within buf
992
992
  unsigned len;
@@ -1009,14 +1009,14 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
1009
1009
  if (len == 0 || (hs.flags & F_ERROR)) {
1010
1010
  char msg[128];
1011
1011
  std::snprintf(msg, sizeof(msg),
1012
- "hookSteal: undecodable instruction at +%zu — refusing to steal", off);
1012
+ "hookSteal: undecodable instruction at +%zu - refusing to steal", off);
1013
1013
  throwAt("HOOK_STEAL", msg);
1014
1014
  return env.Null();
1015
1015
  }
1016
1016
  if (off + len > buf.size()) {
1017
1017
  char msg[128];
1018
1018
  std::snprintf(msg, sizeof(msg),
1019
- "hookSteal: truncated instruction at +%zu — cannot cover %zu bytes", off, minBytes);
1019
+ "hookSteal: truncated instruction at +%zu - cannot cover %zu bytes", off, minBytes);
1020
1020
  throwAt("HOOK_STEAL", msg);
1021
1021
  return env.Null();
1022
1022
  }
@@ -1028,7 +1028,7 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
1028
1028
  si.opcode2 = hs.opcode2;
1029
1029
  si.ripRel = (hs.flags & F_MODRM) && hs.modrm_mod == 0 && hs.modrm_rm == 5;
1030
1030
  // Field sizes: the immediate/disp land at the tail of the instruction.
1031
- // imm is the LAST field; disp precedes it — MinHook's offset math:
1031
+ // imm is the LAST field; disp precedes it - MinHook's offset math:
1032
1032
  // disp field = len - immSize - dispSize (same as len-((flags&0x3C)>>2)-4)
1033
1033
  si.immSize = (hs.flags & F_IMM64) ? 8 :
1034
1034
  (hs.flags & F_IMM32) ? 4 :
@@ -1045,14 +1045,14 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
1045
1045
  if (off < minBytes) {
1046
1046
  char msg[128];
1047
1047
  std::snprintf(msg, sizeof(msg),
1048
- "hookSteal: only %zu decodable bytes at 0x%llX — need >= %zu",
1048
+ "hookSteal: only %zu decodable bytes at 0x%llX - need >= %zu",
1049
1049
  off, static_cast<unsigned long long>(addr), minBytes);
1050
1050
  throwAt("HOOK_STEAL", msg);
1051
1051
  return env.Null();
1052
1052
  }
1053
1053
  const std::size_t stolenLen = off;
1054
1054
 
1055
- // Pass 2: relocate — rewrite position-relative operands in the copied
1055
+ // Pass 2: relocate - rewrite position-relative operands in the copied
1056
1056
  // bytes. `bytes` becomes the relocated copy (what the caller writes to the
1057
1057
  // stub); `original` keeps the pristine bytes for unhook.
1058
1058
  std::vector<std::uint8_t> original(buf.begin(), buf.begin() + stolenLen);
@@ -1091,18 +1091,18 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
1091
1091
  const std::uintptr_t oldTarget =
1092
1092
  static_cast<std::uintptr_t>(addr + si.off + si.len + oldRel);
1093
1093
  // Internal jump: target lands inside the stolen window. The copy
1094
- // preserves the operand→target distance — leave the bytes verbatim.
1094
+ // preserves the operand->target distance - leave the bytes verbatim.
1095
1095
  if (oldTarget >= addr && oldTarget < addr + stolenLen) continue;
1096
1096
  if (si.immSize != 4) {
1097
1097
  return refuse("rel8/rel16 external target cannot be widened in place "
1098
- "(MinHook refuses LOOP/JECXZ-external too) — hook a "
1098
+ "(MinHook refuses LOOP/JECXZ-external too) - hook a "
1099
1099
  "different instruction-aligned site");
1100
1100
  }
1101
1101
  const std::int64_t newRel =
1102
1102
  static_cast<std::int64_t>(oldTarget) -
1103
1103
  (static_cast<std::int64_t>(stub) + si.off + si.len);
1104
1104
  if (newRel < INT32_MIN || newRel > INT32_MAX) {
1105
- return refuse("rel32 rewrite lands out of ±2GB range from the stub — "
1105
+ return refuse("rel32 rewrite lands out of +/-2GB range from the stub - "
1106
1106
  "allocate the stub nearer to the branch target");
1107
1107
  }
1108
1108
  const std::int32_t r32 = static_cast<std::int32_t>(newRel);
@@ -1111,7 +1111,7 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
1111
1111
  }
1112
1112
 
1113
1113
  // RIP-relative ModRM memory operand: the disp32 field is the position-
1114
- // dependent part — rewrite it to reach the same absolute address.
1114
+ // dependent part - rewrite it to reach the same absolute address.
1115
1115
  if (si.ripRel) {
1116
1116
  if (si.dispSize != 4) continue; // mod=00 rm=101 always disp32
1117
1117
  std::int32_t d32 = 0;
@@ -1123,7 +1123,7 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
1123
1123
  static_cast<std::int64_t>(oldTarget) -
1124
1124
  (static_cast<std::int64_t>(stub) + si.off + si.len);
1125
1125
  if (newDisp < INT32_MIN || newDisp > INT32_MAX) {
1126
- return refuse("disp32 rewrite lands out of ±2GB range from the stub — "
1126
+ return refuse("disp32 rewrite lands out of +/-2GB range from the stub - "
1127
1127
  "allocate the stub nearer to the operand target");
1128
1128
  }
1129
1129
  const std::int32_t n32 = static_cast<std::int32_t>(newDisp);
@@ -1144,7 +1144,7 @@ Napi::Value HookStealSync(const Napi::CallbackInfo& info) {
1144
1144
 
1145
1145
  // suspendProcess(pid) / resumeProcess(pid) via undocumented-but-stable
1146
1146
  // NtSuspendProcess / NtResumeProcess exported by ntdll. Atomic whole-process
1147
- // suspend — strictly better than per-thread suspendAll (no TOCTOU where a
1147
+ // suspend - strictly better than per-thread suspendAll (no TOCTOU where a
1148
1148
  // new thread spawns mid-enumeration).
1149
1149
  static Napi::Value NtSuspendResume(const Napi::CallbackInfo& info, bool suspend) {
1150
1150
  Napi::Env env = info.Env();
@@ -1153,11 +1153,11 @@ static Napi::Value NtSuspendResume(const Napi::CallbackInfo& info, bool suspend)
1153
1153
  return env.Null();
1154
1154
  }
1155
1155
  DWORD pid = info[0].As<Napi::Number>().Uint32Value();
1156
- // QA4-C3: suspending OUR OWN process freezes the JS engine — even though
1156
+ // QA4-C3: suspending OUR OWN process freezes the JS engine - even though
1157
1157
  // NtSuspendProcess skips the calling thread, every other thread (including
1158
1158
  // any that would resume us) is suspended and the object can never reach
1159
1159
  // resumeProcess(). Suspend on self-pid is an unrecoverable deadlock; refuse
1160
- // with a clear error. Resume on self-pid is harmless — it just clears
1160
+ // with a clear error. Resume on self-pid is harmless - it just clears
1161
1161
  // suspend counts on our own threads.
1162
1162
  if (suspend && pid == GetCurrentProcessId()) {
1163
1163
  auto err = Napi::Error::New(env,
@@ -1271,13 +1271,13 @@ Napi::Value WaitForExitAsync(const Napi::CallbackInfo& info) {
1271
1271
  // ---------- findWhatWrites (DebugActiveProcess + HW write breakpoints) ----------
1272
1272
 
1273
1273
  // Hardware breakpoints only deliver their #DB exception to a debugger attached
1274
- // to the TARGET — setting Dr regs without a debug loop just crashes the victim
1274
+ // to the TARGET - setting Dr regs without a debug loop just crashes the victim
1275
1275
  // with an unhandled EXCEPTION_SINGLE_STEP. So the CE "find what writes" flow
1276
1276
  // must own a real debugger session: DebugActiveProcess -> arm RW=01 data
1277
1277
  // breakpoints on every thread -> pump WaitForDebugEvent collecting
1278
1278
  // EXCEPTION_SINGLE_STEP hits -> restore Dr regs, ContinueDebugEvent, detach.
1279
1279
  //
1280
- // NOTE on `rip`: a data breakpoint is a *trap* — it fires AFTER the write, so
1280
+ // NOTE on `rip`: a data breakpoint is a *trap* - it fires AFTER the write, so
1281
1281
  // the reported address is the instruction FOLLOWING the write, not the writer
1282
1282
  // itself. CE shows the same caveat; disassemble backwards to find the store.
1283
1283
 
@@ -1379,7 +1379,7 @@ public:
1379
1379
  // protected targets. DebugSetProcessKillOnExit(false) keeps the debuggee
1380
1380
  // alive when our process exits while still attached.
1381
1381
  void Execute() override {
1382
- // Detach completion is asynchronous on Windows — a fresh
1382
+ // Detach completion is asynchronous on Windows - a fresh
1383
1383
  // DebugActiveProcess immediately after DebugActiveProcessStop can return
1384
1384
  // ERROR_ACCESS_DENIED while the kernel tears down the debug port. Retry
1385
1385
  // for up to ~500ms so back-to-back findWhatWrites calls work.
@@ -1399,7 +1399,7 @@ public:
1399
1399
  // QA4-M3 (lifecycle): wrap all post-attach phases so ANY C++ exception
1400
1400
  // (bad_alloc under a flooding target, length_error in hits_/armed_
1401
1401
  // growth) still runs disarm+drain+Stop. Without this the debuggee is
1402
- // left attached with armed Dr7 breakpoints and suspended threads — the
1402
+ // left attached with armed Dr7 breakpoints and suspended threads - the
1403
1403
  // next write to the watched addr would fire an unhandled #DB and kill it.
1404
1404
  try {
1405
1405
  RunPhases();
@@ -1423,7 +1423,7 @@ public:
1423
1423
  // LOAD_DLLs, CREATE_THREAD per existing thread, initial breakpoint) and
1424
1424
  // arm each reported thread. Threads reported by CREATE_THREAD are already
1425
1425
  // debug-suspended at this point; our suspend/resume pair is balanced so
1426
- // the bp is on before they can run. Hits are collected here too — a thread
1426
+ // the bp is on before they can run. Hits are collected here too - a thread
1427
1427
  // armed+continued early in this loop can fire while we drain the rest.
1428
1428
  int quietWaits = 0;
1429
1429
  while (quietWaits < 3 && GetTickCount64() < armDeadline &&
@@ -1433,7 +1433,7 @@ public:
1433
1433
  quietWaits = 0;
1434
1434
  ContinueDebugEvent(ev.dwProcessId, ev.dwThreadId, HandleEvent(ev));
1435
1435
  }
1436
- // Fallback: arm anything the event burst missed (rare — attach reports
1436
+ // Fallback: arm anything the event burst missed (rare - attach reports
1437
1437
  // every thread, but belt-and-suspenders for filtered/racy cases).
1438
1438
  ArmSnapshotThreads();
1439
1439
 
@@ -1446,10 +1446,10 @@ public:
1446
1446
  }
1447
1447
  }
1448
1448
 
1449
- // Phase 3: disarm every armed thread (restoring ORIGINAL Dr regs — the
1449
+ // Phase 3: disarm every armed thread (restoring ORIGINAL Dr regs - the
1450
1450
  // target may legitimately use its own hw breakpoints), drain pending
1451
1451
  // events, disarm any stragglers hit mid-disarm, drain again, detach.
1452
- // Everything runs on this thread — DebugActiveProcess/Continue/DebugStop
1452
+ // Everything runs on this thread - DebugActiveProcess/Continue/DebugStop
1453
1453
  // are debugger-thread-bound. Idempotent: runs only while attached_ and is
1454
1454
  // invoked from every Execute() exit path (normal + exception).
1455
1455
  void CleanupDebuggee() {
@@ -1472,7 +1472,7 @@ public:
1472
1472
  CloseHandle(ev.u.CreateProcessInfo.hFile);
1473
1473
  if (ev.dwDebugEventCode == LOAD_DLL_DEBUG_EVENT && ev.u.LoadDll.hFile)
1474
1474
  CloseHandle(ev.u.LoadDll.hFile);
1475
- // A queued single-step is our bp firing mid-disarm — swallow it
1475
+ // A queued single-step is our bp firing mid-disarm - swallow it
1476
1476
  // (DBG_CONTINUE). A queued genuine exception gets NOT_HANDLED so it
1477
1477
  // reaches the debuggee's SEH as it would without us.
1478
1478
  DWORD disp = DBG_CONTINUE;
@@ -1489,7 +1489,7 @@ public:
1489
1489
  disarmAll();
1490
1490
  drain(); // flush pending bp hits + resume stopped threads
1491
1491
  disarmAll(); // threads stopped on a queued event during pass 1 skipped
1492
- // SetThreadContext — disarm them now that they're resumed
1492
+ // SetThreadContext - disarm them now that they're resumed
1493
1493
  drain(); // release the last stopped threads, empty the port
1494
1494
  DebugActiveProcessStop(pid_);
1495
1495
  attached_ = false;
@@ -1543,7 +1543,7 @@ private:
1543
1543
  const DWORD tid = ev.dwThreadId;
1544
1544
  // QA4-m4: hThread here is system-owned (unlike CREATE_PROCESS's hFile
1545
1545
  // which the debugger must close). Microsoft's debug samples and docs
1546
- // never close it — the system cleans it up on EXIT_THREAD. Verified:
1546
+ // never close it - the system cleans it up on EXIT_THREAD. Verified:
1547
1547
  // do NOT close; closing could later collide with a recycled handle
1548
1548
  // value the debugger also sees. Disarm uses OpenThread-by-tid anyway.
1549
1549
  HANDLE t = ev.u.CreateThread.hThread;
@@ -1560,7 +1560,7 @@ private:
1560
1560
  return DBG_CONTINUE;
1561
1561
  }
1562
1562
  case EXIT_THREAD_DEBUG_EVENT:
1563
- // If it was armed there's nothing to restore — the thread is gone.
1563
+ // If it was armed there's nothing to restore - the thread is gone.
1564
1564
  return DBG_CONTINUE;
1565
1565
  case EXIT_PROCESS_DEBUG_EVENT:
1566
1566
  exited_ = true;
@@ -1690,7 +1690,7 @@ Napi::Value FindWhatWritesAsync(const Napi::CallbackInfo& info) {
1690
1690
  if (std::isfinite(m) && m >= 1) maxHits = m > 1024 ? 1024u : static_cast<unsigned>(m);
1691
1691
  }
1692
1692
 
1693
- // QA4-H7: CONTEXT/Dr registers use the x64 struct — on a WoW64 (32-bit)
1693
+ // QA4-H7: CONTEXT/Dr registers use the x64 struct - on a WoW64 (32-bit)
1694
1694
  // target Get/SetThreadContext operate on WOW64_CONTEXT and the arm would
1695
1695
  // silently corrupt the debuggee or never fire. Refuse up front (same
1696
1696
  // pattern as injectDll).
package/native/pattern.cc CHANGED
@@ -10,7 +10,7 @@ namespace mp {
10
10
  struct PatternByte { std::uint8_t value; bool wildcard; };
11
11
 
12
12
  // Parse a hex-byte pattern ("DE AD ?? EF"). Tokens must be 1-2 hex digits or
13
- // ?/?? wildcards. Returns nullopt on ANY malformed token — the caller turns
13
+ // ?/?? wildcards. Returns nullopt on ANY malformed token - the caller turns
14
14
  // this into a JS error; no C++ exception can cross the N-API boundary.
15
15
  static std::optional<std::vector<PatternByte>> ParsePattern(const std::string& pat) {
16
16
  std::vector<PatternByte> out;
@@ -60,7 +60,7 @@ static inline bool RegionWritable(const MEMORY_BASIC_INFORMATION& mbi) {
60
60
  // Async region-scan worker shared by the AOB scan and the typed-value scan.
61
61
  // Runs with a PRIVATE least-privilege process handle opened from the pid (a
62
62
  // JS-side close() can never invalidate it mid-scan), and scans each committed
63
- // range with a rolling window — 64KB chunks plus a (needle-1)-byte overlap —
63
+ // range with a rolling window - 64KB chunks plus a (needle-1)-byte overlap -
64
64
  // so multi-hundred-MB regions never accumulate in memory.
65
65
  class ScanWorker : public Napi::AsyncWorker {
66
66
  public:
@@ -84,7 +84,7 @@ public:
84
84
  if (!proc_) { SetError("OpenProcess failed (pid gone or access denied)"); return; }
85
85
 
86
86
  // QA4 lifecycle-M1: a C++ exception (bad_alloc in hits_ growth or the
87
- // per-chunk vector) escaping Execute() used to skip CloseHandle — a real
87
+ // per-chunk vector) escaping Execute() used to skip CloseHandle - a real
88
88
  // handle leak under memory pressure, every scan call. Guard all exits.
89
89
  try {
90
90
  const std::uintptr_t limit = end_ ? end_ : USER_TOP;
@@ -156,7 +156,7 @@ private:
156
156
  if (!ReadProcessMemory(proc_, reinterpret_cast<LPCVOID>(pos),
157
157
  chunk.data(), toRead, &got) || got == 0) {
158
158
  tail.clear(); // VA discontinuity: a match can't straddle a failed read
159
- // Don't blindly skip the whole 64KB chunk — a single inaccessible
159
+ // Don't blindly skip the whole 64KB chunk - a single inaccessible
160
160
  // page inside a readable region would drop signatures CE finds.
161
161
  // Re-query the sub-region at `pos`: skip only up to its end, or just
162
162
  // one 4KB page when the sub-region still claims to be readable (the
@@ -185,7 +185,7 @@ private:
185
185
  win.insert(win.end(), tail.end() - static_cast<std::ptrdiff_t>(keep), tail.end());
186
186
  win.insert(win.end(), chunk.begin(), chunk.end());
187
187
  const std::uintptr_t winBase = pos - keep;
188
- // match starts that touch ≥1 new byte: i >= keep - overlap (clamped 0)
188
+ // match starts that touch >=1 new byte: i >= keep - overlap (clamped 0)
189
189
  const std::size_t firstI = keep > overlap ? keep - overlap : 0;
190
190
  if (win.size() >= patLen) {
191
191
  const std::size_t lastI = win.size() - patLen; // inclusive
package/native/process.cc CHANGED
@@ -61,7 +61,7 @@ static std::string WideToUtf8(const wchar_t* w) {
61
61
  }
62
62
 
63
63
  std::vector<ModuleInfo> GetModules(HANDLE /*proc*/, DWORD pid) {
64
- // Toolhelp snapshot — unlike EnumProcessModulesEx this lists BOTH 64-bit and
64
+ // Toolhelp snapshot - unlike EnumProcessModulesEx this lists BOTH 64-bit and
65
65
  // 32-bit (WoW64) modules from a 64-bit reader, has no fixed-cap buffer, and
66
66
  // hands us base+size+path in one shot.
67
67
  std::vector<ModuleInfo> out;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memoryprocess/bindings",
3
- "version": "0.0.1",
3
+ "version": "0.0.3-beta.2",
4
4
  "description": "Native N-API bindings for Windows process memory access (read/write, VirtualQueryEx, pattern scan, thread ops). The .node binary layer of the memoryprocess toolkit.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.4.4",
@@ -31,7 +31,7 @@
31
31
  "clean": "node scripts/clean.mjs",
32
32
  "check": "bun check",
33
33
  "typecheck": "tsc --noEmit",
34
- "prebuild:all": "prebuildify --all --napi",
34
+ "prebuild:all": "prebuildify --napi -t 22.17.0 -t 24.4.0 -t 25.0.0 -t 26.0.0",
35
35
  "prepublishOnly": "npm run build && npm run prebuild:all"
36
36
  },
37
37
  "repository": {