paavohtl Claude Opus 4.6 (1M context) commited on
Commit
ffa35d6
Β·
1 Parent(s): c7eda22

feat: add sub-object hashing to snapshot system

Browse files

Add hash_region_canonical() and hash_pointer_targets() to the snapshot
module. These follow every heap pointer in a struct, hash the first N
bytes of each target with pointer canonicalization, and produce
deterministic fingerprints for cross-run comparison.

Integrated into the snapshot capture() output as [SubObjectHashes].
This is the technique that found the arrow collision region desync β€”
sub-object content differences invisible to flat struct comparisons.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-core/src/snapshot.rs CHANGED
@@ -106,3 +106,65 @@ fn is_likely_pointer(val: u32, delta: u32) -> bool {
106
  }
107
  false
108
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  }
107
  false
108
  }
109
+
110
+ // ── Sub-object hashing ──
111
+
112
+ /// CRC32 hash of a memory region with pointer values replaced by 0.
113
+ ///
114
+ /// Uses [`is_likely_pointer`] to detect pointer-sized values and exclude them,
115
+ /// producing a pointer-independent fingerprint. Useful for A/B comparison of
116
+ /// objects across runs with different heap layouts.
117
+ #[cfg(target_arch = "x86")]
118
+ pub unsafe fn hash_region_canonical(ptr: *const u8, len: usize) -> u32 {
119
+ let delta = rb(va::IMAGE_BASE).wrapping_sub(va::IMAGE_BASE);
120
+ let mut h: u32 = 0xFFFF_FFFF;
121
+ for i in (0..len).step_by(4) {
122
+ let val = if i + 4 <= len {
123
+ *(ptr.add(i) as *const u32)
124
+ } else { 0 };
125
+ let hash_val = if is_likely_pointer(val, delta) { 0u32 } else { val };
126
+ for byte in hash_val.to_le_bytes() {
127
+ h ^= byte as u32;
128
+ for _ in 0..8 {
129
+ h = if h & 1 != 0 { (h >> 1) ^ 0xEDB88320 } else { h >> 1 };
130
+ }
131
+ }
132
+ }
133
+ !h
134
+ }
135
+
136
+ /// Hash all sub-objects reachable from a base struct via pointer fields.
137
+ ///
138
+ /// Walks every DWORD in `base[0..struct_size]`. For each non-null value that
139
+ /// looks like a heap pointer, hashes the first `target_window` bytes of the
140
+ /// target using [`hash_region_canonical`]. Writes `[HASH] name+OFFSET hash=XX`
141
+ /// lines to the output.
142
+ ///
143
+ /// This is the tool that found the arrow collision region desync: sub-objects
144
+ /// pointed to by DDGame had different non-pointer content between the Rust
145
+ /// and original constructors, invisible to flat DDGame comparisons.
146
+ #[cfg(target_arch = "x86")]
147
+ pub unsafe fn hash_pointer_targets(
148
+ w: &mut dyn fmt::Write,
149
+ base: *const u8,
150
+ struct_size: usize,
151
+ target_window: usize,
152
+ name: &str,
153
+ ) -> fmt::Result {
154
+ let delta = rb(va::IMAGE_BASE).wrapping_sub(va::IMAGE_BASE);
155
+
156
+ for off in (0..struct_size).step_by(4) {
157
+ let val = *(base.add(off) as *const u32);
158
+ if val == 0 { continue; }
159
+ if !is_likely_pointer(val, delta) { continue; }
160
+ // Skip WA code/data pointers β€” only follow heap pointers
161
+ let ghidra = val.wrapping_sub(delta);
162
+ if ghidra >= va::TEXT_START && ghidra < va::DATA_END { continue; }
163
+ // Verify target is readable
164
+ if !mem::can_read(val, target_window as u32) { continue; }
165
+
166
+ let h = hash_region_canonical(val as *const u8, target_window);
167
+ writeln!(w, "[HASH] {}+0x{:04X} hash={:08X}", name, off, h)?;
168
+ }
169
+ Ok(())
170
+ }
crates/openwa-wormkit/src/snapshot.rs CHANGED
@@ -8,7 +8,7 @@ use core::fmt::Write;
8
  use openwa_core::address::va;
9
  use openwa_core::engine::GameSession;
10
  use openwa_core::rebase::rb;
11
- use openwa_core::snapshot::{write_raw_region, Snapshot};
12
  use openwa_core::task::{CTask, CTaskBfsIter, CTaskMissile, CTaskWorm};
13
 
14
  /// Capture a full game state snapshot as text.
@@ -43,6 +43,18 @@ pub unsafe fn capture() -> String {
43
  let _ = (*ddgame).write_snapshot(&mut out, 1);
44
  let _ = writeln!(out);
45
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  // ── Team blocks + worm entries ──
47
  let team_count = (*ddgame).team_arena.team_count as usize;
48
  let _ = writeln!(out, "[Teams] count={}", team_count);
 
8
  use openwa_core::address::va;
9
  use openwa_core::engine::GameSession;
10
  use openwa_core::rebase::rb;
11
+ use openwa_core::snapshot::{hash_pointer_targets, write_raw_region, Snapshot};
12
  use openwa_core::task::{CTask, CTaskBfsIter, CTaskMissile, CTaskWorm};
13
 
14
  /// Capture a full game state snapshot as text.
 
43
  let _ = (*ddgame).write_snapshot(&mut out, 1);
44
  let _ = writeln!(out);
45
 
46
+ // ── Sub-object hashes (pointer-independent) ──
47
+ // Hashes the first 256 bytes of every heap object pointed to by DDGame
48
+ // and PCLandscape. Differences here indicate sub-object state mismatches
49
+ // that flat DDGame comparisons miss.
50
+ let _ = writeln!(out, "[SubObjectHashes]");
51
+ let _ = hash_pointer_targets(&mut out, ddgame as *const u8, 0x550, 256, "ddgame");
52
+ let landscape = (*ddgame).landscape as *const u8;
53
+ if !landscape.is_null() {
54
+ let _ = hash_pointer_targets(&mut out, landscape, 0xB44, 256, "landscape");
55
+ }
56
+ let _ = writeln!(out);
57
+
58
  // ── Team blocks + worm entries ──
59
  let team_count = (*ddgame).team_arena.team_count as usize;
60
  let _ = writeln!(out, "[Teams] count={}", team_count);