paavohtl Claude Opus 4.6 (1M context) commited on
Commit
c3b0ade
·
1 Parent(s): f48d57c

feat: add live object tracker with field-level pointer identification

Browse files

Live object tracker in registry.rs enables detect_pointer() to resolve
heap pointers into "DDGame.rng_state" style names:

- register_live_object() / unregister_live_object() for tracking
- identify_live_pointer() checks if a pointer falls inside any tracked
object and resolves the field name via StructFields
- detect_pointer() now checks live objects before vtable fallback

DDGameWrapper, DDGame, and GameSession are registered during
construct_ddgame_wrapper() in the wormkit DLL.

Also fixes proc macro to submit StructRegistration via inventory,
enabling struct_fields_for("DDGame") global lookups.

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

crates/openwa-core/src/mem.rs CHANGED
@@ -185,7 +185,32 @@ pub unsafe fn detect_pointer(value: u32, delta: u32) -> Option<KnownPointer> {
185
  }
186
  }
187
 
188
- // 2. If heap pointer, check if first DWORD is a known vtable
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  if can_read(value, 4) {
190
  let first = *(value as *const u32);
191
  let ghidra_first = first.wrapping_sub(delta);
@@ -201,7 +226,7 @@ pub unsafe fn detect_pointer(value: u32, delta: u32) -> Option<KnownPointer> {
201
  }
202
  }
203
 
204
- // 3. Fall back to segment-based classification
205
  classify_pointer(value, delta).map(|info| KnownPointer {
206
  raw_value: info.raw_value,
207
  ghidra_value: info.ghidra_value,
 
185
  }
186
  }
187
 
188
+ // 2. Check if pointer falls inside a tracked live object
189
+ if let Some(m) = registry::identify_live_pointer(value) {
190
+ let name = match m.field {
191
+ Some(field) if m.offset == field.offset => {
192
+ format!("{}.{}", m.object.class_name, field.name)
193
+ }
194
+ Some(field) => {
195
+ let inner_off = m.offset - field.offset;
196
+ format!("{}.{}+0x{:X}", m.object.class_name, field.name, inner_off)
197
+ }
198
+ None => format!("{}+0x{:X}", m.object.class_name, m.offset),
199
+ };
200
+ return Some(KnownPointer {
201
+ raw_value: value,
202
+ ghidra_value: ghidra_val,
203
+ segment: PointerKind::Object,
204
+ name: Some(name),
205
+ class_name: Some(m.object.class_name),
206
+ detail: Some(format!(
207
+ "base=0x{:08X} offset=0x{:X}",
208
+ m.object.ptr, m.offset
209
+ )),
210
+ });
211
+ }
212
+
213
+ // 3. If heap pointer, check if first DWORD is a known vtable
214
  if can_read(value, 4) {
215
  let first = *(value as *const u32);
216
  let ghidra_first = first.wrapping_sub(delta);
 
226
  }
227
  }
228
 
229
+ // 4. Fall back to segment-based classification
230
  classify_pointer(value, delta).map(|info| KnownPointer {
231
  raw_value: info.raw_value,
232
  ghidra_value: info.ghidra_value,
crates/openwa-core/src/registry.rs CHANGED
@@ -277,6 +277,91 @@ pub fn all_struct_fields() -> impl Iterator<Item = &'static StructFields> {
277
  struct_map().values().copied()
278
  }
279
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  #[cfg(test)]
281
  mod tests {
282
  use super::*;
 
277
  struct_map().values().copied()
278
  }
279
 
280
+ // =========================================================================
281
+ // Live object tracker
282
+ // =========================================================================
283
+
284
+ /// A tracked live heap object.
285
+ #[derive(Debug, Clone)]
286
+ pub struct LiveObject {
287
+ /// Runtime base address.
288
+ pub ptr: u32,
289
+ /// Object size in bytes (0 if unknown).
290
+ pub size: u32,
291
+ /// Class/struct name (e.g., "DDGame").
292
+ pub class_name: &'static str,
293
+ /// Field registry for this struct (if available).
294
+ pub fields: Option<&'static StructFields>,
295
+ }
296
+
297
+ use std::sync::Mutex;
298
+
299
+ static LIVE_OBJECTS: Mutex<Vec<LiveObject>> = Mutex::new(Vec::new());
300
+
301
+ /// Register a live heap object for pointer identification.
302
+ ///
303
+ /// Call this from constructor hooks when a game object is allocated.
304
+ pub fn register_live_object(obj: LiveObject) {
305
+ if let Ok(mut v) = LIVE_OBJECTS.lock() {
306
+ // Replace if same pointer already tracked (re-allocation)
307
+ if let Some(existing) = v.iter_mut().find(|o| o.ptr == obj.ptr) {
308
+ *existing = obj;
309
+ } else {
310
+ v.push(obj);
311
+ }
312
+ }
313
+ }
314
+
315
+ /// Unregister a live heap object (e.g., on destruction).
316
+ pub fn unregister_live_object(ptr: u32) {
317
+ if let Ok(mut v) = LIVE_OBJECTS.lock() {
318
+ v.retain(|o| o.ptr != ptr);
319
+ }
320
+ }
321
+
322
+ /// Result of identifying a pointer as being inside a tracked live object.
323
+ #[derive(Debug)]
324
+ pub struct LiveObjectMatch {
325
+ pub object: LiveObject,
326
+ /// Byte offset of the pointer within the object.
327
+ pub offset: u32,
328
+ /// Field at this offset (if known).
329
+ pub field: Option<&'static FieldEntry>,
330
+ }
331
+
332
+ /// Check if a runtime pointer falls inside any tracked live object.
333
+ ///
334
+ /// Returns the matching object and the field at the pointer's offset.
335
+ pub fn identify_live_pointer(runtime_ptr: u32) -> Option<LiveObjectMatch> {
336
+ let v = LIVE_OBJECTS.lock().ok()?;
337
+ for obj in v.iter() {
338
+ let end = if obj.size > 0 {
339
+ obj.ptr + obj.size
340
+ } else {
341
+ // Unknown size — use a generous 256KB bound
342
+ obj.ptr + 0x40000
343
+ };
344
+ if runtime_ptr >= obj.ptr && runtime_ptr < end {
345
+ let offset = runtime_ptr - obj.ptr;
346
+ let field = obj.fields.and_then(|f| {
347
+ f.field_at(offset)
348
+ .or_else(|| f.field_containing(offset).map(|(fe, _)| fe))
349
+ });
350
+ return Some(LiveObjectMatch {
351
+ object: obj.clone(),
352
+ offset,
353
+ field,
354
+ });
355
+ }
356
+ }
357
+ None
358
+ }
359
+
360
+ /// Return a snapshot of all currently tracked live objects.
361
+ pub fn live_objects() -> Vec<LiveObject> {
362
+ LIVE_OBJECTS.lock().map(|v| v.clone()).unwrap_or_default()
363
+ }
364
+
365
  #[cfg(test)]
366
  mod tests {
367
  use super::*;
crates/openwa-wormkit/src/replacements/game_session.rs CHANGED
@@ -97,6 +97,17 @@ pub(crate) unsafe fn construct_ddgame_wrapper(
97
  let timer_obj = (*session).timer_obj;
98
  let net_game = (*session).net_game;
99
 
 
 
 
 
 
 
 
 
 
 
 
100
  let _ = log_line(&format!(
101
  "[GameSession] display=0x{:08X}, net_game=0x{:08X}, timer=0x{:08X}, game_info(EDI)=0x{:08X}",
102
  display as u32, net_game as u32, timer_obj as u32, game_info as u32,
@@ -147,6 +158,23 @@ pub(crate) unsafe fn construct_ddgame_wrapper(
147
  this as u32, (*this).ddgame as u32,
148
  ));
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  this
151
  }
152
 
 
97
  let timer_obj = (*session).timer_obj;
98
  let net_game = (*session).net_game;
99
 
100
+ // Register GameSession as a live object.
101
+ {
102
+ use openwa_core::registry::{self, LiveObject};
103
+ registry::register_live_object(LiveObject {
104
+ ptr: session as u32,
105
+ size: 0x120,
106
+ class_name: "GameSession",
107
+ fields: registry::struct_fields_for("GameSession"),
108
+ });
109
+ }
110
+
111
  let _ = log_line(&format!(
112
  "[GameSession] display=0x{:08X}, net_game=0x{:08X}, timer=0x{:08X}, game_info(EDI)=0x{:08X}",
113
  display as u32, net_game as u32, timer_obj as u32, game_info as u32,
 
158
  this as u32, (*this).ddgame as u32,
159
  ));
160
 
161
+ // Register live objects for pointer identification in debug tools.
162
+ use openwa_core::registry::{self, LiveObject};
163
+ registry::register_live_object(LiveObject {
164
+ ptr: this as u32,
165
+ size: core::mem::size_of::<DDGameWrapper>() as u32,
166
+ class_name: "DDGameWrapper",
167
+ fields: registry::struct_fields_for("DDGameWrapper"),
168
+ });
169
+ if !(*this).ddgame.is_null() {
170
+ registry::register_live_object(LiveObject {
171
+ ptr: (*this).ddgame as u32,
172
+ size: 0x98D8, // DDGame size
173
+ class_name: "DDGame",
174
+ fields: registry::struct_fields_for("DDGame"),
175
+ });
176
+ }
177
+
178
  this
179
  }
180