From 6aa17f57324847890fc6fac5a69b50edeb69210a Mon Sep 17 00:00:00 2001 From: Andy Jost Date: Mon, 15 Jun 2026 12:06:18 -0700 Subject: [PATCH 1/3] cuda.core: warn on unpickling IPC buffers from untrusted pickle (V13.1) Document the pickle-to-IPC-import trust boundary and emit a one-time UserWarning when unpickling a Buffer reconstructs via from_ipc_descriptor (Glasswing V13.1). --- SECURITY.md | 11 +++++ cuda_core/cuda/core/_memory/_buffer.pyi | 13 ++++++ cuda_core/cuda/core/_memory/_buffer.pyx | 15 ++++++ .../core/_memory/_device_memory_resource.pyi | 11 ++++- .../core/_memory/_device_memory_resource.pyx | 11 ++++- cuda_core/cuda/core/_memory/_ipc.pyi | 9 +++- cuda_core/cuda/core/_memory/_ipc.pyx | 9 +++- cuda_core/cuda/core/_utils/cuda_utils.pyi | 7 +++ cuda_core/cuda/core/_utils/cuda_utils.pyx | 25 ++++++++++ cuda_core/tests/test_ipc_pickle_warning.py | 46 +++++++++++++++++++ 10 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 cuda_core/tests/test_ipc_pickle_warning.py diff --git a/SECURITY.md b/SECURITY.md index 42835415566..63ef6e4950c 100755 --- a/SECURITY.md +++ b/SECURITY.md @@ -29,6 +29,17 @@ disclosure policy. Please visit our [Product Security Incident Response Team (PSIRT)](https://www.nvidia.com/en-us/security/psirt-policies/) policies page for more information. +## CUDA IPC and Python serialization + +`cuda.core.Buffer` objects allocated from IPC-enabled memory resources can be +pickled for transfer between same-host processes. Unpickling performs an IPC +memory import using the embedded `IPCBufferDescriptor`. Only unpickle buffers +(and call `Buffer.from_ipc_descriptor`) with descriptors from trusted peers; +malicious descriptors can trigger invalid memory operations. + +When sharing CUDA objects across processes, use `multiprocessing` with the +`spawn` start method. + ## NVIDIA Product Security For all security-related concerns, please visit NVIDIA's Product Security portal at diff --git a/cuda_core/cuda/core/_memory/_buffer.pyi b/cuda_core/cuda/core/_memory/_buffer.pyi index 728853c4bc7..99b73c98bcf 100644 --- a/cuda_core/cuda/core/_memory/_buffer.pyi +++ b/cuda_core/cuda/core/_memory/_buffer.pyi @@ -19,6 +19,13 @@ class Buffer: allocations. Support for data interchange mechanisms are provided by DLPack. + + Note + ---- + Pickling an IPC-enabled :class:`Buffer` embeds an + :class:`~_memory.IPCBufferDescriptor`. Unpickling reconstructs the buffer + by calling :meth:`from_ipc_descriptor` and therefore performs an IPC + import. Do not unpickle buffers from untrusted sources. """ def __cinit__(self) -> None: @@ -85,6 +92,12 @@ class Buffer: stream : :obj:`~_stream.Stream` Keyword-only. The stream used for asynchronous deallocation when the buffer is closed or garbage collected. + + Note + ---- + The descriptor payload and ``size`` are supplied by the exporting peer + and must be treated as untrusted input unless the peer is known to be + cooperating. """ @property diff --git a/cuda_core/cuda/core/_memory/_buffer.pyx b/cuda_core/cuda/core/_memory/_buffer.pyx index 88f9054385a..05cc047c7d3 100644 --- a/cuda_core/cuda/core/_memory/_buffer.pyx +++ b/cuda_core/cuda/core/_memory/_buffer.pyx @@ -26,6 +26,7 @@ from cuda.core.typing import DevicePointerType from cuda.core._stream cimport Stream, Stream_accept, default_stream from cuda.core._utils.cuda_utils cimport HANDLE_RETURN, _parse_fill_value +from cuda.core._utils.cuda_utils import warn_ipc_buffer_unpickle import sys from typing import TYPE_CHECKING @@ -86,6 +87,13 @@ cdef class Buffer: allocations. Support for data interchange mechanisms are provided by DLPack. + + Note + ---- + Pickling an IPC-enabled :class:`Buffer` embeds an + :class:`~_memory.IPCBufferDescriptor`. Unpickling reconstructs the buffer + by calling :meth:`from_ipc_descriptor` and therefore performs an IPC + import. Do not unpickle buffers from untrusted sources. """ def __cinit__(self) -> None: self._clear() @@ -135,6 +143,7 @@ cdef class Buffer: # pickle path cannot thread an explicit stream through. Seed the # imported buffer's deallocation with the current context's default # stream; the receiver can override via buffer.close(stream). + warn_ipc_buffer_unpickle() return Buffer.from_ipc_descriptor(mr, ipc_descriptor, stream=default_stream()) def __reduce__(self) -> tuple[object, ...]: @@ -187,6 +196,12 @@ cdef class Buffer: stream : :obj:`~_stream.Stream` Keyword-only. The stream used for asynchronous deallocation when the buffer is closed or garbage collected. + + Note + ---- + The descriptor payload and ``size`` are supplied by the exporting peer + and must be treated as untrusted input unless the peer is known to be + cooperating. """ return _ipc.Buffer_from_ipc_descriptor(cls, mr, ipc_descriptor, stream) diff --git a/cuda_core/cuda/core/_memory/_device_memory_resource.pyi b/cuda_core/cuda/core/_memory/_device_memory_resource.pyi index 033ff7c5333..21862b32b7a 100644 --- a/cuda_core/cuda/core/_memory/_device_memory_resource.pyi +++ b/cuda_core/cuda/core/_memory/_device_memory_resource.pyi @@ -105,7 +105,16 @@ class DeviceMemoryResource(_MemPool): an MMR is created and registered in the receiving process. Subsequently, buffers may be serialized and transferred using ordinary :mod:`pickle` methods. The reconstruction procedure uses the registry to find the - associated MMR. + associated MMR. Unpickling a :class:`Buffer` performs an IPC import from + the embedded descriptor; only unpickle buffers received from trusted peers. + + Warning + ------- + IPC descriptors and pickled buffers cross a trust boundary between + cooperating same-host processes. A malicious peer can supply crafted + descriptor fields. Use :meth:`Buffer.from_ipc_descriptor` only with + descriptors from trusted peers, and do not unpickle buffers from + untrusted sources. """ def __cinit__(self, *args, **kwargs) -> None: diff --git a/cuda_core/cuda/core/_memory/_device_memory_resource.pyx b/cuda_core/cuda/core/_memory/_device_memory_resource.pyx index 13f654e305d..45d8d543ac4 100644 --- a/cuda_core/cuda/core/_memory/_device_memory_resource.pyx +++ b/cuda_core/cuda/core/_memory/_device_memory_resource.pyx @@ -130,7 +130,16 @@ cdef class DeviceMemoryResource(_MemPool): an MMR is created and registered in the receiving process. Subsequently, buffers may be serialized and transferred using ordinary :mod:`pickle` methods. The reconstruction procedure uses the registry to find the - associated MMR. + associated MMR. Unpickling a :class:`Buffer` performs an IPC import from + the embedded descriptor; only unpickle buffers received from trusted peers. + + Warning + ------- + IPC descriptors and pickled buffers cross a trust boundary between + cooperating same-host processes. A malicious peer can supply crafted + descriptor fields. Use :meth:`Buffer.from_ipc_descriptor` only with + descriptors from trusted peers, and do not unpickle buffers from + untrusted sources. """ def __cinit__(self, *args, **kwargs) -> None: diff --git a/cuda_core/cuda/core/_memory/_ipc.pyi b/cuda_core/cuda/core/_memory/_ipc.pyi index 9e5be61af72..0c912a567bd 100644 --- a/cuda_core/cuda/core/_memory/_ipc.pyi +++ b/cuda_core/cuda/core/_memory/_ipc.pyi @@ -38,7 +38,14 @@ class IPCDataForMR: ... class IPCBufferDescriptor: - """Serializable object describing a buffer that can be shared between processes.""" + """Serializable object describing a buffer that can be shared between processes. + + Note + ---- + The payload and ``size`` fields are controlled by the exporting peer. + Receivers must treat them as untrusted and import only through + :meth:`Buffer.from_ipc_descriptor`. + """ def __init__(self, *arg, **kwargs) -> None: ... diff --git a/cuda_core/cuda/core/_memory/_ipc.pyx b/cuda_core/cuda/core/_memory/_ipc.pyx index 61fd8b086e7..7baf6e94bbf 100644 --- a/cuda_core/cuda/core/_memory/_ipc.pyx +++ b/cuda_core/cuda/core/_memory/_ipc.pyx @@ -78,7 +78,14 @@ cdef class IPCDataForMR: cdef class IPCBufferDescriptor: - """Serializable object describing a buffer that can be shared between processes.""" + """Serializable object describing a buffer that can be shared between processes. + + Note + ---- + The payload and ``size`` fields are controlled by the exporting peer. + Receivers must treat them as untrusted and import only through + :meth:`Buffer.from_ipc_descriptor`. + """ def __init__(self, *arg, **kwargs) -> None: raise RuntimeError("IPCBufferDescriptor objects cannot be instantiated directly. Please use MemoryResource APIs.") diff --git a/cuda_core/cuda/core/_utils/cuda_utils.pyi b/cuda_core/cuda/core/_utils/cuda_utils.pyi index 87067927724..6874bab5673 100644 --- a/cuda_core/cuda/core/_utils/cuda_utils.pyi +++ b/cuda_core/cuda/core/_utils/cuda_utils.pyi @@ -60,6 +60,7 @@ _keep_nvrtc_in_stub: 'nvrtc.nvrtcResult' _keep_runtime_in_stub: 'runtime.cudaError_t' ComputeCapability = namedtuple('ComputeCapability', ('major', 'minor')) _fork_warning_checked = False +_ipc_pickle_warning_checked = False def _check_driver_error(error: cydriver.CUresult) -> int: ... @@ -140,5 +141,11 @@ def reset_fork_warning() -> None: to check the warning behavior. """ +def reset_ipc_pickle_warning() -> None: + """Reset the IPC buffer unpickle warning flag for testing purposes.""" + +def warn_ipc_buffer_unpickle() -> None: + """Warn that unpickling a Buffer performs an IPC import from embedded data.""" + def check_multiprocessing_start_method() -> None: """Check if multiprocessing start method is 'fork' and warn if so.""" \ No newline at end of file diff --git a/cuda_core/cuda/core/_utils/cuda_utils.pyx b/cuda_core/cuda/core/_utils/cuda_utils.pyx index 4e20f689b5a..993e3fbfe42 100644 --- a/cuda_core/cuda/core/_utils/cuda_utils.pyx +++ b/cuda_core/cuda/core/_utils/cuda_utils.pyx @@ -348,6 +348,9 @@ class Transaction: # Track whether we've already warned about fork method _fork_warning_checked = False +# Track whether we've already warned about unpickling IPC buffers +_ipc_pickle_warning_checked = False + def reset_fork_warning() -> None: """Reset the fork warning check flag for testing purposes. @@ -359,6 +362,28 @@ def reset_fork_warning() -> None: _fork_warning_checked = False +def reset_ipc_pickle_warning() -> None: + """Reset the IPC buffer unpickle warning flag for testing purposes.""" + global _ipc_pickle_warning_checked + _ipc_pickle_warning_checked = False + + +def warn_ipc_buffer_unpickle() -> None: + """Warn that unpickling a Buffer performs an IPC import from embedded data.""" + global _ipc_pickle_warning_checked + if _ipc_pickle_warning_checked: + return + _ipc_pickle_warning_checked = True + message = ( + "Unpickling a cuda.core.Buffer imports GPU memory via the embedded " + "IPCBufferDescriptor. Only unpickle Buffer objects received from a " + "trusted same-host peer process; malicious pickle payloads can supply " + "crafted descriptors. Prefer explicit Buffer.from_ipc_descriptor only " + "with descriptors from cooperating peers." + ) + warnings.warn(message, UserWarning, stacklevel=4) + + cdef inline tuple _read_fill_ptr(const char* ptr, Py_ssize_t width): """Extract (value, element_size) from a raw pointer of known width.""" cdef unsigned int val diff --git a/cuda_core/tests/test_ipc_pickle_warning.py b/cuda_core/tests/test_ipc_pickle_warning.py new file mode 100644 index 00000000000..5eab3ea9813 --- /dev/null +++ b/cuda_core/tests/test_ipc_pickle_warning.py @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Test warnings when unpickling IPC-enabled Buffer objects.""" + +import warnings + +from cuda.core import Buffer +from cuda.core._utils.cuda_utils import reset_ipc_pickle_warning, warn_ipc_buffer_unpickle + +NBYTES = 64 + + +def test_warn_on_buffer_unpickle(ipc_device, ipc_memory_resource): + """Unpickling an IPC buffer warns about the trust boundary.""" + mr = ipc_memory_resource + buf = mr.allocate(NBYTES, stream=ipc_device.default_stream) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + reset_ipc_pickle_warning() + Buffer._reduce_helper(mr, buf.ipc_descriptor) + + assert len(w) == 1, f"Expected 1 warning, got {len(w)}: {[str(x.message) for x in w]}" + warning = w[0] + assert warning.category is UserWarning + assert "unpickl" in str(warning.message).lower() + assert "ipc" in str(warning.message).lower() + assert "trusted" in str(warning.message).lower() + + +def test_ipc_pickle_warning_emitted_only_once(ipc_device, ipc_memory_resource): + """The unpickle trust-boundary warning is emitted at most once per process.""" + mr = ipc_memory_resource + buf1 = mr.allocate(NBYTES, stream=ipc_device.default_stream) + buf2 = mr.allocate(NBYTES, stream=ipc_device.default_stream) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + reset_ipc_pickle_warning() + Buffer._reduce_helper(mr, buf1.ipc_descriptor) + Buffer._reduce_helper(mr, buf2.ipc_descriptor) + warn_ipc_buffer_unpickle() + + ipc_warnings = [x for x in w if "unpickl" in str(x.message).lower()] + assert len(ipc_warnings) == 1 From ea230073cfb65ca2e3c199b4689c5ee86d8443cf Mon Sep 17 00:00:00 2001 From: Andy Jost Date: Mon, 15 Jun 2026 12:17:57 -0700 Subject: [PATCH 2/3] cuda.core: drop unpickle UserWarning; document trust boundary only NVBUG V13.1 mitigation is documentation, not a per-unpickle warning. Legitimate multiprocessing IPC buffer sharing would spam UserWarning on every round-trip. --- cuda_core/cuda/core/_memory/_buffer.pyx | 5 ++- cuda_core/cuda/core/_utils/cuda_utils.pyi | 7 ---- cuda_core/cuda/core/_utils/cuda_utils.pyx | 25 ------------ cuda_core/tests/test_ipc_pickle_warning.py | 46 ---------------------- 4 files changed, 3 insertions(+), 80 deletions(-) delete mode 100644 cuda_core/tests/test_ipc_pickle_warning.py diff --git a/cuda_core/cuda/core/_memory/_buffer.pyx b/cuda_core/cuda/core/_memory/_buffer.pyx index 05cc047c7d3..6fa7b81f93a 100644 --- a/cuda_core/cuda/core/_memory/_buffer.pyx +++ b/cuda_core/cuda/core/_memory/_buffer.pyx @@ -26,7 +26,6 @@ from cuda.core.typing import DevicePointerType from cuda.core._stream cimport Stream, Stream_accept, default_stream from cuda.core._utils.cuda_utils cimport HANDLE_RETURN, _parse_fill_value -from cuda.core._utils.cuda_utils import warn_ipc_buffer_unpickle import sys from typing import TYPE_CHECKING @@ -143,10 +142,12 @@ cdef class Buffer: # pickle path cannot thread an explicit stream through. Seed the # imported buffer's deallocation with the current context's default # stream; the receiver can override via buffer.close(stream). - warn_ipc_buffer_unpickle() return Buffer.from_ipc_descriptor(mr, ipc_descriptor, stream=default_stream()) def __reduce__(self) -> tuple[object, ...]: + # Security note (CWE-502): unpickling a Buffer performs a live CUDA IPC + # import using descriptor bytes from the pickle stream. Only deserialize + # Buffers from a principal at least as trusted as this process. # Must not serialize the parent's stream! return Buffer._reduce_helper, (self.memory_resource, self.ipc_descriptor) diff --git a/cuda_core/cuda/core/_utils/cuda_utils.pyi b/cuda_core/cuda/core/_utils/cuda_utils.pyi index 6874bab5673..87067927724 100644 --- a/cuda_core/cuda/core/_utils/cuda_utils.pyi +++ b/cuda_core/cuda/core/_utils/cuda_utils.pyi @@ -60,7 +60,6 @@ _keep_nvrtc_in_stub: 'nvrtc.nvrtcResult' _keep_runtime_in_stub: 'runtime.cudaError_t' ComputeCapability = namedtuple('ComputeCapability', ('major', 'minor')) _fork_warning_checked = False -_ipc_pickle_warning_checked = False def _check_driver_error(error: cydriver.CUresult) -> int: ... @@ -141,11 +140,5 @@ def reset_fork_warning() -> None: to check the warning behavior. """ -def reset_ipc_pickle_warning() -> None: - """Reset the IPC buffer unpickle warning flag for testing purposes.""" - -def warn_ipc_buffer_unpickle() -> None: - """Warn that unpickling a Buffer performs an IPC import from embedded data.""" - def check_multiprocessing_start_method() -> None: """Check if multiprocessing start method is 'fork' and warn if so.""" \ No newline at end of file diff --git a/cuda_core/cuda/core/_utils/cuda_utils.pyx b/cuda_core/cuda/core/_utils/cuda_utils.pyx index 993e3fbfe42..4e20f689b5a 100644 --- a/cuda_core/cuda/core/_utils/cuda_utils.pyx +++ b/cuda_core/cuda/core/_utils/cuda_utils.pyx @@ -348,9 +348,6 @@ class Transaction: # Track whether we've already warned about fork method _fork_warning_checked = False -# Track whether we've already warned about unpickling IPC buffers -_ipc_pickle_warning_checked = False - def reset_fork_warning() -> None: """Reset the fork warning check flag for testing purposes. @@ -362,28 +359,6 @@ def reset_fork_warning() -> None: _fork_warning_checked = False -def reset_ipc_pickle_warning() -> None: - """Reset the IPC buffer unpickle warning flag for testing purposes.""" - global _ipc_pickle_warning_checked - _ipc_pickle_warning_checked = False - - -def warn_ipc_buffer_unpickle() -> None: - """Warn that unpickling a Buffer performs an IPC import from embedded data.""" - global _ipc_pickle_warning_checked - if _ipc_pickle_warning_checked: - return - _ipc_pickle_warning_checked = True - message = ( - "Unpickling a cuda.core.Buffer imports GPU memory via the embedded " - "IPCBufferDescriptor. Only unpickle Buffer objects received from a " - "trusted same-host peer process; malicious pickle payloads can supply " - "crafted descriptors. Prefer explicit Buffer.from_ipc_descriptor only " - "with descriptors from cooperating peers." - ) - warnings.warn(message, UserWarning, stacklevel=4) - - cdef inline tuple _read_fill_ptr(const char* ptr, Py_ssize_t width): """Extract (value, element_size) from a raw pointer of known width.""" cdef unsigned int val diff --git a/cuda_core/tests/test_ipc_pickle_warning.py b/cuda_core/tests/test_ipc_pickle_warning.py deleted file mode 100644 index 5eab3ea9813..00000000000 --- a/cuda_core/tests/test_ipc_pickle_warning.py +++ /dev/null @@ -1,46 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Test warnings when unpickling IPC-enabled Buffer objects.""" - -import warnings - -from cuda.core import Buffer -from cuda.core._utils.cuda_utils import reset_ipc_pickle_warning, warn_ipc_buffer_unpickle - -NBYTES = 64 - - -def test_warn_on_buffer_unpickle(ipc_device, ipc_memory_resource): - """Unpickling an IPC buffer warns about the trust boundary.""" - mr = ipc_memory_resource - buf = mr.allocate(NBYTES, stream=ipc_device.default_stream) - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - reset_ipc_pickle_warning() - Buffer._reduce_helper(mr, buf.ipc_descriptor) - - assert len(w) == 1, f"Expected 1 warning, got {len(w)}: {[str(x.message) for x in w]}" - warning = w[0] - assert warning.category is UserWarning - assert "unpickl" in str(warning.message).lower() - assert "ipc" in str(warning.message).lower() - assert "trusted" in str(warning.message).lower() - - -def test_ipc_pickle_warning_emitted_only_once(ipc_device, ipc_memory_resource): - """The unpickle trust-boundary warning is emitted at most once per process.""" - mr = ipc_memory_resource - buf1 = mr.allocate(NBYTES, stream=ipc_device.default_stream) - buf2 = mr.allocate(NBYTES, stream=ipc_device.default_stream) - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - reset_ipc_pickle_warning() - Buffer._reduce_helper(mr, buf1.ipc_descriptor) - Buffer._reduce_helper(mr, buf2.ipc_descriptor) - warn_ipc_buffer_unpickle() - - ipc_warnings = [x for x in w if "unpickl" in str(x.message).lower()] - assert len(ipc_warnings) == 1 From 3d5b917f9f746a22778c017bae31b2e18e958b12 Mon Sep 17 00:00:00 2001 From: Andy Jost Date: Mon, 15 Jun 2026 12:22:28 -0700 Subject: [PATCH 3/3] cuda.core: use plain-English unpickle trust note on __reduce__ Drop CWE-502 label from inline comment; tracker vocabulary belongs in SECURITY.md and NVBUG disposition, not production code. --- cuda_core/cuda/core/_memory/_buffer.pyx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cuda_core/cuda/core/_memory/_buffer.pyx b/cuda_core/cuda/core/_memory/_buffer.pyx index 6fa7b81f93a..a7bb096ce7a 100644 --- a/cuda_core/cuda/core/_memory/_buffer.pyx +++ b/cuda_core/cuda/core/_memory/_buffer.pyx @@ -145,9 +145,8 @@ cdef class Buffer: return Buffer.from_ipc_descriptor(mr, ipc_descriptor, stream=default_stream()) def __reduce__(self) -> tuple[object, ...]: - # Security note (CWE-502): unpickling a Buffer performs a live CUDA IPC - # import using descriptor bytes from the pickle stream. Only deserialize - # Buffers from a principal at least as trusted as this process. + # Unpickling performs a live CUDA IPC import from descriptor bytes in the + # pickle stream. Only deserialize Buffers from a trusted principal. # Must not serialize the parent's stream! return Buffer._reduce_helper, (self.memory_resource, self.ipc_descriptor)