Resolve an object lifetime bug in add_polyline - #1283
Conversation
8040f0c to
8bffa2b
Compare
8bffa2b to
45d6e22
Compare
45d6e22 to
7e2980f
Compare
7e2980f to
13ada06
Compare
There was a problem hiding this comment.
Pull request overview
This PR resolves a lifetime/ownership issue when adding polylines between collections by moving the underlying “polyline/polygon” implementation to a C++ rd::Polygon type stored and shared via std::shared_ptr, and by updating both C++ and Python bindings/callers to use that shared ownership model.
Changes:
- Replace the legacy C-style
geo_polygon_typeAPI with a C++rd::Polygonclass and propagate that through region/grid selection and polygon collections. - Switch Python
CPolylinefrom a pure-Pythoncwrapwrapper to a dedicated pybind11 extension module (resdata.geometry.cpolyline) plus a.pyistub. - Simplify
CPolylineCollection.addPolyline()/shallowCopy()to rely on shared ownership instead of reference/wrapper mechanics.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/geometry_tests/test_cpolyline_collection.py | Renames/adjusts a collection-to-collection add test for the new ownership model. |
| python/resdata/geometry/cpolyline.pyi | Adds typing stubs for the new CPolyline extension module. |
| python/resdata/geometry/cpolyline.py | Removes the old pure-Python CPolyline wrapper implementation. |
| python/resdata/geometry/cpolyline_collection.py | Updates collection add/copy logic to use the new _add_polyline signature and shared ownership. |
| lib/tests/test_geometry.cpp | Migrates polygon tests to use rd::Polygon APIs instead of legacy C functions. |
| lib/resdata/rd_region.cpp | Updates region polygon selection internals to accept/use rd::Polygon. |
| lib/resdata/rd_region_pybind.cpp | Updates Python bindings for region polygon selection to take rd::Polygon. |
| lib/resdata/rd_grid.cpp | Updates XY-in-cell helpers to build/query rd::Polygon instead of C polygons. |
| lib/resdata/fault_block.cpp | Updates fault block polyline intersection checks to use rd::Polygon. |
| lib/resdata/cwrap_pybind.cpp | Removes CPolyline-specific from_cwrap<geo_polygon_type> conversion now that the C type is removed. |
| lib/private-include/detail/resdata/cwrap_pybind.hpp | Removes the CPolyline() helper declaration. |
| lib/include/resdata/rd_region.hpp | Updates the public C API signatures to take rd::Polygon. |
| lib/include/ert/geometry/geo_region.hpp | Updates geo_region polygon selection signatures to take rd::Polygon. |
| lib/include/ert/geometry/geo_polygon.hpp | Replaces the old C polygon API declarations with the rd::Polygon class interface. |
| lib/include/ert/geometry/geo_polygon_collection.hpp | Updates polygon collection APIs to store/return std::shared_ptr<rd::Polygon>. |
| lib/geometry/geo_region.cpp | Updates region selection implementation to call rd::Polygon::contains_point. |
| lib/geometry/geo_region_pybind.cpp | Updates geo_region Python bindings to accept rd::Polygon. |
| lib/geometry/geo_polygon.cpp | Ports polygon algorithms (contains/intersects/length/load) onto rd::Polygon. |
| lib/geometry/geo_polygon_pybind.cpp | Introduces the new cpolyline pybind module exposing CPolyline backed by rd::Polygon. |
| lib/geometry/geo_polygon_collection.cpp | Refactors polygon collection storage to std::vector + std::map of shared_ptrs. |
| lib/geometry/geo_polygon_collection_pybind.cpp | Updates polyline-collection bindings to return/store shared_ptr polygons directly. |
| lib/CMakeLists.txt | Renames the built pybind module from _cpolyline to cpolyline. |
Suppressed comments (3)
lib/geometry/geo_polygon_pybind.cpp:81
__getitem__uses an undefined variable (i) for bounds checking and indexing, which will fail to compile and also breaks negative-index handling. Convert the Python index to a signed integer, validate against the container size, and then index the C++ vector using asize_tcast.
if (index < py::int_{0} || i >= size)
throw py::index_error(fmt::format(
"Invalid index:{} valid range: [0,{})", i, size));
return self[i];
lib/geometry/geo_polygon_pybind.cpp:132
- The
py::class_definition chain is accidentally terminated after__radd__(});), so the following.def(...)calls are no longer attached to any object and will not compile. Keep the chain alive here so later.def(...)calls remain on the samepy::class_expression.
});
.def(py::self == py::self)
.def("segmentLength",
lib/geometry/geo_polygon_pybind.cpp:222
connect()previously returned a Pythonlistof two points, but the new binding returns atupleviapy::make_tuple(...). That is a user-visible API change and also disagrees with the new stub (cpolyline.pyideclaresconnect(...) -> list[tuple[float, float]]). Return apy::list(and also terminate the.def(...)chain with a semicolon).
if (d1.cast<double>() < d2.cast<double>())
return py::make_tuple(end1_tup, p1);
else
return py::make_tuple(end2_tup, p2);
})
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
d2a23e6 to
c1a4d9c
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.
Suppressed comments (3)
lib/resdata/rd_region_pybind.cpp:374
- The keyword argument name for the polygon parameter is set to
region, which is misleading and breaks keyword-based calls. It should bepolygon(matching the lambda parameter).
rd_region_select_outside_polygon(from_cwrap<rd_region_type>(self),
polygon);
},
py::arg("self"), py::arg("region").none(false));
lib/resdata/rd_region_pybind.cpp:381
- The keyword argument name for the polygon parameter is set to
region, which is misleading and breaks keyword-based calls. It should bepolygon(matching the lambda parameter).
rd_region_deselect_inside_polygon(from_cwrap<rd_region_type>(self),
polygon);
},
py::arg("self"), py::arg("region").none(false));
lib/resdata/rd_region_pybind.cpp:388
- The keyword argument name for the polygon parameter is set to
region, which is misleading and breaks keyword-based calls. It should bepolygon(matching the lambda parameter).
rd_region_deselect_outside_polygon(from_cwrap<rd_region_type>(self),
polygon);
},
py::arg("self"), py::arg("region").none(false));
b35f2a4 to
6fc4e28
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 23 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
lib/geometry/geo_polygon_collection.cpp:82
- polygon_list.at() takes a size_t index; passing an int relies on an implicit signed-to-unsigned conversion that can trigger sign-conversion/narrowing diagnostics. Casting explicitly (and/or validating index >= 0) avoids new warnings and makes the intent clear.
return polygons->polygon_list.at(index);
tests/geometry_tests/test_cpolyline_collection.py:167
- This test exercises cross-collection addPolyline(), but it doesn’t assert the lifetime behavior implied by the PR title (i.e., that the polyline remains valid after the source collection and temporary references are released). Adding an explicit
del/gc.collect()and then accessing the polyline fromtargetwould guard against regressions of the original use-after-free.
target = CPolylineCollection()
target.addPolyline(reference)
assert "border" in target
assert len(target) == 1
6fc4e28 to
8cd2f07
Compare
This avoids a object lifetime bug in geo_polygon_collection
8cd2f07 to
8211d43
Compare
ajaust
left a comment
There was a problem hiding this comment.
Nice work. I left some comments.
| void geo_polygon_free(geo_polygon_type *polygon) { delete polygon; } | ||
|
|
||
| void geo_polygon_free__(void *arg) { | ||
| geo_polygon_type *polygon = geo_polygon_safe_cast(arg); |
There was a problem hiding this comment.
Typo? The commit message refers to "safe case", but we remove a "save cast".
| if (size() == 1) | ||
| return 0; | ||
| else { | ||
| double length = 0; | ||
| double x0 = polygon->xcoord.at(0); | ||
| double y0 = polygon->ycoord.at(0); | ||
|
|
||
| for (size_t i = 1; i < polygon->xcoord.size(); i++) { | ||
| double x1 = polygon->xcoord.at(i); | ||
| double y1 = polygon->ycoord.at(i); | ||
| auto [x0, y0] = points[0]; |
There was a problem hiding this comment.
If length() is called on an empty Polygon we access the points vector even though it is empty.
rd::Polygon polygon;
std::cout << polygon.length() << std::endl; // Out-of-bounds access to points[0], but points.size() is 0|
|
||
|
|
||
| def test_that_a_reference_polyline_can_be_added_to_another_collection(): | ||
| def test_that_a_polyline_from_one_collection_can_be_added_to_another_collection(): |
There was a problem hiding this comment.
Can we add any tests that would trigger the lifetime bug to ensure that it is not introduced again in the future?
| polyline = CPolyline(init_points=[(0, 0), (1, 1)]) | ||
|
|
||
| with pytest.raises(TypeError, match="Index argument must be integer"): | ||
| with pytest.raises(TypeError): |
There was a problem hiding this comment.
Why can't we match against the error message anymore?
No description provided.