[Bugfix] GridLayer::parent_grid is a dangling raw pointer when grid is destroyed #270

Closed
opened 2026-03-07 23:22:36 +00:00 by john · 2 comments
Owner

Summary

GridLayer stores its parent grid as a raw UIGrid* pointer (GridLayers.h:34). When the owning grid is destroyed, this pointer is not automatically nullified. If a Python-side reference to the layer outlives the grid, any method that accesses parent_grid dereferences freed memory.

Root Cause

GridLayers.h:34:

class GridLayer {
    ...
    UIGrid* parent_grid;   // Parent grid reference
    ...
};

This raw pointer is used extensively in GridLayers.cpp:

  • Line 304, 309, 313: FOV computation via parent_grid->computeFOV()
  • Line 319: FOV query via parent_grid->isInFOV()
  • Line 361, 366, 367: Perspective update via parent_grid->fov_radius

While some access points check if (!parent_grid), the pointer itself becomes dangling (non-NULL but invalid) when the grid is destroyed — the check passes but the dereference is UB.

Reproduction

import mcrfpy

grid = mcrfpy.Grid(grid_size=(10, 10))
layer = mcrfpy.TileLayer(name="terrain", z_index=0, texture=some_texture)
grid.add_layer(layer)

# Keep reference to layer, destroy grid
del grid
# layer.parent_grid is now dangling

# Any operation that accesses parent_grid will crash
layer.grid = another_grid  # Setter reads parent_grid internally

Note: The grid setter code (GridLayers.cpp:1624, 2259) does check if (!self->data->parent_grid), but a dangling pointer won't be NULL — it will appear valid and dereference freed memory.

Fix Options

  1. Use std::shared_ptr<UIGrid>: Keeps grid alive while layers reference it — but creates ownership issues since grid owns layers
  2. Use std::weak_ptr<UIGrid>: Best option. Grid stays as owner, layers hold weak references. Check .lock() before access.
  3. Invalidation on destruction: Grid destructor sets all layers' parent_grid = nullptr. Requires tracking.

Option 2 (weak_ptr) is cleanest: it mirrors the pattern already used by PyUniformCollectionObject::owner.

Severity

Medium — requires grid to be destroyed while layer references exist. Currently uncommon in practice because grids tend to live longer than their layers, but becomes important with dynamic scene management.

## Summary `GridLayer` stores its parent grid as a raw `UIGrid*` pointer (`GridLayers.h:34`). When the owning grid is destroyed, this pointer is not automatically nullified. If a Python-side reference to the layer outlives the grid, any method that accesses `parent_grid` dereferences freed memory. ## Root Cause `GridLayers.h:34`: ```cpp class GridLayer { ... UIGrid* parent_grid; // Parent grid reference ... }; ``` This raw pointer is used extensively in `GridLayers.cpp`: - Line 304, 309, 313: FOV computation via `parent_grid->computeFOV()` - Line 319: FOV query via `parent_grid->isInFOV()` - Line 361, 366, 367: Perspective update via `parent_grid->fov_radius` While some access points check `if (!parent_grid)`, the pointer itself becomes dangling (non-NULL but invalid) when the grid is destroyed — the check passes but the dereference is UB. ## Reproduction ```python import mcrfpy grid = mcrfpy.Grid(grid_size=(10, 10)) layer = mcrfpy.TileLayer(name="terrain", z_index=0, texture=some_texture) grid.add_layer(layer) # Keep reference to layer, destroy grid del grid # layer.parent_grid is now dangling # Any operation that accesses parent_grid will crash layer.grid = another_grid # Setter reads parent_grid internally ``` **Note**: The grid setter code (`GridLayers.cpp:1624, 2259`) does check `if (!self->data->parent_grid)`, but a dangling pointer won't be NULL — it will appear valid and dereference freed memory. ## Fix Options 1. **Use `std::shared_ptr<UIGrid>`**: Keeps grid alive while layers reference it — but creates ownership issues since grid owns layers 2. **Use `std::weak_ptr<UIGrid>`**: Best option. Grid stays as owner, layers hold weak references. Check `.lock()` before access. 3. **Invalidation on destruction**: Grid destructor sets all layers' `parent_grid = nullptr`. Requires tracking. Option 2 (weak_ptr) is cleanest: it mirrors the pattern already used by `PyUniformCollectionObject::owner`. ## Severity **Medium** — requires grid to be destroyed while layer references exist. Currently uncommon in practice because grids tend to live longer than their layers, but becomes important with dynamic scene management.
Author
Owner

Deferred from the #258–#278 memory safety audit. The raw UIGrid* pointer issue is naturally resolved by #252 (GridMap/GridView split), which replaces UIGrid entirely. GridLayer::parent_grid will change from UIGrid* to GridMap* with proper lifetime management as part of that architecture change. Linked as dependency of #252.

Deferred from the #258–#278 memory safety audit. The raw `UIGrid*` pointer issue is naturally resolved by #252 (GridMap/GridView split), which replaces `UIGrid` entirely. GridLayer::parent_grid will change from `UIGrid*` to `GridMap*` with proper lifetime management as part of that architecture change. Linked as dependency of #252.
Author
Owner

Roadmap context

Folded into #252 (Grid/GridView split) in the Grid & Entity Overhaul Roadmap (docs/GRID_ENTITY_OVERHAUL_ROADMAP.md), Phase 4. During the split, all raw UIGrid* pointers in GridLayer, UIGridPoint, and GridChunk will be converted to weak_ptr<Grid>.

## Roadmap context Folded into #252 (Grid/GridView split) in the Grid & Entity Overhaul Roadmap (`docs/GRID_ENTITY_OVERHAUL_ROADMAP.md`), **Phase 4**. During the split, all raw `UIGrid*` pointers in GridLayer, UIGridPoint, and GridChunk will be converted to `weak_ptr<Grid>`.
john closed this issue 2026-04-10 05:46:54 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
john/McRogueFace#270
No description provided.