Skip to content

Commit 6eb900e

Browse files
committed
fix(core): validate Host(is_numa_current=...) as a bool
`Host.__new__` validates `numa_id` carefully -- including an explicit bool rejection whose comment spells out why: # bool is an int subclass; reject explicitly so Host(True) doesn't # alias Host(1) (and vice versa) in the singleton cache. `is_numa_current` gets no validation at all, even though it lands in the same process-wide singleton cache key and is stored verbatim as the `is_numa_current` property. The same aliasing hazard applies from the other side: >>> Host(is_numa_current=1) # int, not True >>> h = Host.numa_current() >>> h.is_numa_current 1 >>> h.is_numa_current is True False `(None, 1)` and `(None, True)` are the same dict key, so whichever call runs first wins for the lifetime of the process -- and `test_host.py` already asserts `Host.numa_current().is_numa_current is True`, which that first call turns into an order-dependent failure. Non-int truthy values are worse: they mint a second singleton instead of poisoning the first. >>> dup = Host(is_numa_current="yes") >>> repr(dup) 'Host.numa_current()' >>> dup is Host.numa_current(), dup == Host.numa_current() (False, False) Both coerce to the same `_LocSpec(kind="host_numa_current")`, so they name one physical location while breaking the documented "constructor calls with the same arguments return the same instance" contract and the identity-based set arithmetic in `ManagedBuffer.accessed_by`. `is_numa_current=0` and `=None` are equally unhandled: they are falsy, so they seed the *generic* `Host()` singleton with a non-bool `is_numa_current`. And an unhashable value reaches the cache lookup and raises `TypeError: unhashable type` out of the constructor. Require a real bool, mirroring the numa_id check.
1 parent 3bd069a commit 6eb900e

3 files changed

Lines changed: 40 additions & 0 deletions

File tree

cuda_core/cuda/core/_host.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ def __new__(cls, numa_id: int | None = None, *, is_numa_current: bool = False) -
4747
raise ValueError("numa_id and is_numa_current are mutually exclusive")
4848
if numa_id is not None and (isinstance(numa_id, bool) or not isinstance(numa_id, int) or numa_id < 0):
4949
raise ValueError(f"numa_id must be a non-negative int, got {numa_id!r}")
50+
# Require a real bool for the same reason numa_id rejects one: the
51+
# value goes straight into the singleton cache key and into the
52+
# is_numa_current property. `1` and `True` are the same dict key, so
53+
# `Host(is_numa_current=1)` would hand `Host.numa_current()` an
54+
# instance whose is_numa_current is the int 1 for the rest of the
55+
# process, and any other truthy value would create a second,
56+
# non-identical "numa_current" singleton.
57+
if not isinstance(is_numa_current, bool):
58+
raise ValueError(f"is_numa_current must be a bool, got {is_numa_current!r}")
5059
return cls._get_or_create(numa_id, is_numa_current)
5160

5261
@classmethod

cuda_core/docs/source/release/1.2.0-notes.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ Fixes and enhancements
7373
Windows, both ``ctypes.CFUNCTYPE`` and ``ctypes.WINFUNCTYPE`` are accepted.
7474
(`#2439 <https://github.com/NVIDIA/cuda-python/issues/2439>`__)
7575

76+
- :class:`Host` now requires ``is_numa_current`` to be a ``bool``, matching the
77+
existing ``numa_id`` validation. The value was stored verbatim in the
78+
process-wide singleton cache and in :attr:`Host.is_numa_current`, so
79+
``Host(is_numa_current=1)`` made :meth:`Host.numa_current` return an instance
80+
whose ``is_numa_current`` was the int ``1``, and any other truthy value
81+
created a second ``Host.numa_current()`` that compared unequal to the real
82+
one while sharing its ``repr``.
83+
7684
Deprecation Notices
7785
-------------------
7886

cuda_core/tests/test_host.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,29 @@ def test_numa_id_rejects_bool(self):
3434
with pytest.raises(ValueError, match="numa_id must be a non-negative int"):
3535
Host(numa_id=False)
3636

37+
@pytest.mark.agent_authored(model="claude-opus-5")
38+
@pytest.mark.parametrize("value", [1, 0, "yes", None, []], ids=["int-1", "int-0", "str", "none", "list"])
39+
def test_is_numa_current_rejects_non_bool(self, value):
40+
# Same hazard as test_numa_id_rejects_bool, from the other side: the
41+
# value lands in the singleton cache key and in the is_numa_current
42+
# property untouched. `1` and `True` hash and compare equal, so
43+
# Host(is_numa_current=1) would seed the numa_current singleton with
44+
# an instance whose is_numa_current is the int 1 -- process-wide, and
45+
# for whichever call happens to come first. Any other truthy value
46+
# would mint a second "numa_current" that is neither `is` nor `==` the
47+
# real one while sharing its repr.
48+
with pytest.raises(ValueError, match="is_numa_current must be a bool"):
49+
Host(is_numa_current=value)
50+
51+
@pytest.mark.agent_authored(model="claude-opus-5")
52+
def test_numa_current_singleton_survives_a_rejected_construction(self):
53+
with pytest.raises(ValueError, match="is_numa_current must be a bool"):
54+
Host(is_numa_current=1)
55+
56+
h = Host.numa_current()
57+
assert h.is_numa_current is True
58+
assert h is Host(is_numa_current=True)
59+
3760
def test_numa_current_constructor_and_classmethod_agree(self):
3861
# Host(is_numa_current=True) and Host.numa_current() return the same singleton.
3962
assert Host(is_numa_current=True) is Host.numa_current()

0 commit comments

Comments
 (0)