From cd34ce8a6ff1fd1c2bba038fb41f395bd8be9ca3 Mon Sep 17 00:00:00 2001 From: 0z5a Date: Thu, 5 Nov 2026 10:45:39 +0100 Subject: [PATCH 1/8] gh-161387: Check the type of the decimal context ContextVar --- Lib/test/test_decimal.py | 10 ++++++++++ .../2026-11-05-10-05-00.gh-issue-161387.def456.rst | 2 ++ Modules/_decimal/_decimal.c | 6 ++++++ 3 files changed, 18 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-11-05-10-05-00.gh-issue-161387.def456.rst diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index d049646d6e8b55a..b8ed14dbbd95c66 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -3766,6 +3766,16 @@ def test_to_integral_value(self): @requires_cdecimal class CContextAPItests(ContextAPItests, unittest.TestCase): decimal = C + + def test_current_context_var_wrong_type(self): + # gh-161387: the internal ContextVar must hold a Context instance. + ctx = self.decimal.getcontext() + var = next(v for v in contextvars.copy_context() + if v.name == "decimal_context") + self.addCleanup(var.set, ctx) + var.set(1) + with self.assertRaises(TypeError): + self.decimal.Decimal(1) + self.decimal.Decimal(2) class PyContextAPItests(ContextAPItests, unittest.TestCase): decimal = P diff --git a/Misc/NEWS.d/next/Library/2026-11-05-10-05-00.gh-issue-161387.def456.rst b/Misc/NEWS.d/next/Library/2026-11-05-10-05-00.gh-issue-161387.def456.rst new file mode 100644 index 000000000000000..f445980563853bb --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-11-05-10-05-00.gh-issue-161387.def456.rst @@ -0,0 +1,2 @@ +Fix a crash in :mod:`decimal` when the internal ``decimal_context`` +:class:`contextvars.ContextVar` holds a value that is not a context. diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index 4849b447e277bfc..3f33aea23085b3b 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -1997,6 +1997,12 @@ current_context(decimal_state *state) } if (tl_context != NULL) { + if (!PyDecContext_Check(state, tl_context)) { + PyErr_SetString(PyExc_TypeError, + "current decimal context is not a Context"); + Py_DECREF(tl_context); + return NULL; + } return tl_context; } From 0254fa8b5667ade46832379c9a2eaced1ed829d0 Mon Sep 17 00:00:00 2001 From: 0z5a Date: Thu, 5 Nov 2026 10:46:54 +0100 Subject: [PATCH 2/8] Fix test isolation for the duplicate decimal_context var --- Lib/test/test_decimal.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index b8ed14dbbd95c66..064ae850aa01666 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -3769,11 +3769,14 @@ class CContextAPItests(ContextAPItests, unittest.TestCase): def test_current_context_var_wrong_type(self): # gh-161387: the internal ContextVar must hold a Context instance. - ctx = self.decimal.getcontext() - var = next(v for v in contextvars.copy_context() - if v.name == "decimal_context") - self.addCleanup(var.set, ctx) - var.set(1) + # Both _decimal and _pydecimal define a var with this name, so set + # every matching var and restore them with their reset tokens. + matches = [(var, var.set(1)) + for var in contextvars.copy_context() + if var.name == "decimal_context"] + self.assertTrue(matches) + self.addCleanup(lambda: [var.reset(token) + for var, token in matches]) with self.assertRaises(TypeError): self.decimal.Decimal(1) + self.decimal.Decimal(2) class PyContextAPItests(ContextAPItests, unittest.TestCase): From c10e2e7b9e88df2ce4cbd6c5852d7f8739727e45 Mon Sep 17 00:00:00 2001 From: 0z5a Date: Thu, 5 Nov 2026 12:18:40 +0100 Subject: [PATCH 3/8] Address review: report the received type, tidy the test --- Lib/test/test_decimal.py | 20 +++++++++++--------- Modules/_decimal/_decimal.c | 6 ++++-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 064ae850aa01666..79c423842e71347 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -3768,15 +3768,17 @@ class CContextAPItests(ContextAPItests, unittest.TestCase): decimal = C def test_current_context_var_wrong_type(self): - # gh-161387: the internal ContextVar must hold a Context instance. - # Both _decimal and _pydecimal define a var with this name, so set - # every matching var and restore them with their reset tokens. - matches = [(var, var.set(1)) - for var in contextvars.copy_context() - if var.name == "decimal_context"] - self.assertTrue(matches) - self.addCleanup(lambda: [var.reset(token) - for var, token in matches]) + # gh-161387 + context_vars = [var for var in contextvars.copy_context() + if var.name == "decimal_context"] + self.assertTrue(context_vars) + tokens = [var.set(1) for var in context_vars] + + def restore(): + for var, token in zip(context_vars, tokens): + var.reset(token) + + self.addCleanup(restore) with self.assertRaises(TypeError): self.decimal.Decimal(1) + self.decimal.Decimal(2) class PyContextAPItests(ContextAPItests, unittest.TestCase): diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index 3f33aea23085b3b..3cb07610b1ada4a 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -1998,8 +1998,10 @@ current_context(decimal_state *state) if (tl_context != NULL) { if (!PyDecContext_Check(state, tl_context)) { - PyErr_SetString(PyExc_TypeError, - "current decimal context is not a Context"); + PyErr_Format(PyExc_TypeError, + "current decimal context must be a Context " + "instance, got %.200s", + Py_TYPE(tl_context)->tp_name); Py_DECREF(tl_context); return NULL; } From 18682f0a016469c942598ecc41572cfbe012cf4b Mon Sep 17 00:00:00 2001 From: 0z5a Date: Thu, 5 Nov 2026 12:29:02 +0100 Subject: [PATCH 4/8] Skip the test when _decimal has no context variables --- Lib/test/test_decimal.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 79c423842e71347..dd6165d8a91a553 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -3769,6 +3769,8 @@ class CContextAPItests(ContextAPItests, unittest.TestCase): def test_current_context_var_wrong_type(self): # gh-161387 + if not self.decimal.HAVE_CONTEXTVAR: + self.skipTest("_decimal built without context variables") context_vars = [var for var in contextvars.copy_context() if var.name == "decimal_context"] self.assertTrue(context_vars) From 6c4cfcb91dbd53bfa86f9152f86cc580ef17efdb Mon Sep 17 00:00:00 2001 From: 0z5a Date: Thu, 5 Nov 2026 12:37:26 +0100 Subject: [PATCH 5/8] Update Modules/_decimal/_decimal.c Co-authored-by: sobolevn --- Modules/_decimal/_decimal.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index 3cb07610b1ada4a..130a0cd97e5e991 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -2000,8 +2000,7 @@ current_context(decimal_state *state) if (!PyDecContext_Check(state, tl_context)) { PyErr_Format(PyExc_TypeError, "current decimal context must be a Context " - "instance, got %.200s", - Py_TYPE(tl_context)->tp_name); + "instance, got %T", tl_context); Py_DECREF(tl_context); return NULL; } From 360d1667639a68178add5311bd9b231d9a1a710a Mon Sep 17 00:00:00 2001 From: 0z5a Date: Thu, 5 Nov 2026 12:37:39 +0100 Subject: [PATCH 6/8] Update Lib/test/test_decimal.py Co-authored-by: sobolevn --- Lib/test/test_decimal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index dd6165d8a91a553..5b621e20db285bf 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -3783,6 +3783,7 @@ def restore(): self.addCleanup(restore) with self.assertRaises(TypeError): self.decimal.Decimal(1) + self.decimal.Decimal(2) + class PyContextAPItests(ContextAPItests, unittest.TestCase): decimal = P From 589b9f2ef7c8ab55fff7d58b8cf438bda2320ed1 Mon Sep 17 00:00:00 2001 From: 0z5a Date: Thu, 5 Nov 2026 12:39:20 +0100 Subject: [PATCH 7/8] Test that the wrong-typed context is not leaked --- Lib/test/test_decimal.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 5b621e20db285bf..86b07d1f4308187 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -3771,18 +3771,22 @@ def test_current_context_var_wrong_type(self): # gh-161387 if not self.decimal.HAVE_CONTEXTVAR: self.skipTest("_decimal built without context variables") + value = object() context_vars = [var for var in contextvars.copy_context() if var.name == "decimal_context"] self.assertTrue(context_vars) - tokens = [var.set(1) for var in context_vars] + tokens = [var.set(value) for var in context_vars] def restore(): for var, token in zip(context_vars, tokens): var.reset(token) self.addCleanup(restore) - with self.assertRaises(TypeError): - self.decimal.Decimal(1) + self.decimal.Decimal(2) + refcount = sys.getrefcount(value) + for _ in range(3): + with self.assertRaises(TypeError): + self.decimal.Decimal(1) + self.decimal.Decimal(2) + self.assertEqual(sys.getrefcount(value), refcount) class PyContextAPItests(ContextAPItests, unittest.TestCase): decimal = P From 7bbee0df95dd9b48234bf2d0d2fe5d64b07eac8e Mon Sep 17 00:00:00 2001 From: 0z5a Date: Thu, 5 Nov 2026 12:47:17 +0100 Subject: [PATCH 8/8] Assert the context var is unchanged and check the error message --- Lib/test/test_decimal.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 86b07d1f4308187..b5905d1080ea7a9 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -3782,11 +3782,10 @@ def restore(): var.reset(token) self.addCleanup(restore) - refcount = sys.getrefcount(value) - for _ in range(3): - with self.assertRaises(TypeError): - self.decimal.Decimal(1) + self.decimal.Decimal(2) - self.assertEqual(sys.getrefcount(value), refcount) + with self.assertRaisesRegex(TypeError, "got object"): + self.decimal.Decimal(1) + self.decimal.Decimal(2) + for var in context_vars: + self.assertIs(var.get(), value) class PyContextAPItests(ContextAPItests, unittest.TestCase): decimal = P