From 27e35e3ac3fd4c04999b8819fb8dc883a9e18d70 Mon Sep 17 00:00:00 2001 From: Sarah Hoffmann Date: Thu, 15 Oct 2026 20:07:13 +0200 Subject: [PATCH] avoid segfault when OSM object constructer is interrupted Thanks to @t-miller for catching the issue and proposing a test. --- lib/osm_base_objects.h | 4 +- test/test_constructor_interruption.py | 54 +++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 test/test_constructor_interruption.py diff --git a/lib/osm_base_objects.h b/lib/osm_base_objects.h index 50e3a6be..243965c1 100644 --- a/lib/osm_base_objects.h +++ b/lib/osm_base_objects.h @@ -2,7 +2,7 @@ * * This file is part of pyosmium. (https://osmcode.org/pyosmium/) * - * Copyright (C) 2024 Sarah Hoffmann and others. + * Copyright (C) 2026 Sarah Hoffmann and others. * For a full list of authors see the git log. */ #ifndef PYOSMIUM_OSM_BASE_OBJECTS_HPP @@ -73,7 +73,6 @@ class PyOSMObject pybind11::object get_or_create_python_object() { if (!m_valid) { - m_valid = true; if constexpr (std::is_same_v) { m_pyobj = pybind11::module_::import("osmium.osm.types").attr("Node")(COSMNode{m_obj}); } else if constexpr (std::is_same_v) { @@ -85,6 +84,7 @@ class PyOSMObject } else if constexpr (std::is_same_v) { m_pyobj = pybind11::module_::import("osmium.osm.types").attr("Changeset")(COSMChangeset{m_obj}); } + m_valid = true; } return m_pyobj; diff --git a/test/test_constructor_interruption.py b/test/test_constructor_interruption.py new file mode 100644 index 00000000..6e685b08 --- /dev/null +++ b/test/test_constructor_interruption.py @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# This file is part of pyosmium. (https://osmcode.org/pyosmium/) +# Copyright (C) 2026 Sarah Hoffmann and others. +# For a full list of authors see the git log. +import multiprocessing +import sys + +import pytest +import osmium + + +def run_interrupted_read(pipe, object_type, opl, exception_type): + constructor = object_type.__init__.__code__ + + def fail_constructor(frame, event, arg): + if event == 'call' and frame.f_code is constructor: + sys.settrace(None) + raise exception_type('injected constructor failure') + return fail_constructor + + sys.settrace(fail_constructor) + try: + list(osmium.FileProcessor(osmium.io.FileBuffer(opl.encode('utf-8'), 'opl'))) + except exception_type: + sys.settrace(None) + pipe.send(str(exception_type)) + else: + pipe.send('constructor was not reached') + + +@pytest.mark.thread_unsafe +@pytest.mark.parametrize('object_type', [osmium.osm.Node, osmium.osm.Way, osmium.osm.Relation]) +@pytest.mark.parametrize('exception', [KeyboardInterrupt, MemoryError, RuntimeError]) +def test_constructor_exception_does_not_crash_cleanup(to_opl, object_type, exception): + data = to_opl("""\ + n1 x5 y5 + w1 Nn1,n2 + r1 Mn1@ + """) + + parent_conn, child_conn = multiprocessing.Pipe() + ctx = multiprocessing.get_context('spawn') + p = ctx.Process(target=run_interrupted_read, + args=[child_conn, object_type, data, exception]) + p.start() + + assert parent_conn.poll(2) + assert parent_conn.recv() == str(exception) + + p.join(2) + + assert p.exitcode == 0, f"Segmentation fault ({-p.exitcode})" if p.exitcode < 0 \ + else f"Test exited with error {p.exitcode}"