Skip to content

Commit 6aa17f5

Browse files
committed
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).
1 parent 25abc36 commit 6aa17f5

10 files changed

Lines changed: 153 additions & 4 deletions

File tree

SECURITY.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ disclosure policy. Please visit our [Product Security Incident Response Team
2929
(PSIRT)](https://www.nvidia.com/en-us/security/psirt-policies/) policies page for more
3030
information.
3131

32+
## CUDA IPC and Python serialization
33+
34+
`cuda.core.Buffer` objects allocated from IPC-enabled memory resources can be
35+
pickled for transfer between same-host processes. Unpickling performs an IPC
36+
memory import using the embedded `IPCBufferDescriptor`. Only unpickle buffers
37+
(and call `Buffer.from_ipc_descriptor`) with descriptors from trusted peers;
38+
malicious descriptors can trigger invalid memory operations.
39+
40+
When sharing CUDA objects across processes, use `multiprocessing` with the
41+
`spawn` start method.
42+
3243
## NVIDIA Product Security
3344

3445
For all security-related concerns, please visit NVIDIA's Product Security portal at

cuda_core/cuda/core/_memory/_buffer.pyi

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ class Buffer:
1919
allocations.
2020
2121
Support for data interchange mechanisms are provided by DLPack.
22+
23+
Note
24+
----
25+
Pickling an IPC-enabled :class:`Buffer` embeds an
26+
:class:`~_memory.IPCBufferDescriptor`. Unpickling reconstructs the buffer
27+
by calling :meth:`from_ipc_descriptor` and therefore performs an IPC
28+
import. Do not unpickle buffers from untrusted sources.
2229
"""
2330

2431
def __cinit__(self) -> None:
@@ -85,6 +92,12 @@ class Buffer:
8592
stream : :obj:`~_stream.Stream`
8693
Keyword-only. The stream used for asynchronous deallocation when
8794
the buffer is closed or garbage collected.
95+
96+
Note
97+
----
98+
The descriptor payload and ``size`` are supplied by the exporting peer
99+
and must be treated as untrusted input unless the peer is known to be
100+
cooperating.
88101
"""
89102

90103
@property

cuda_core/cuda/core/_memory/_buffer.pyx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ from cuda.core.typing import DevicePointerType
2626

2727
from cuda.core._stream cimport Stream, Stream_accept, default_stream
2828
from cuda.core._utils.cuda_utils cimport HANDLE_RETURN, _parse_fill_value
29+
from cuda.core._utils.cuda_utils import warn_ipc_buffer_unpickle
2930

3031
import sys
3132
from typing import TYPE_CHECKING
@@ -86,6 +87,13 @@ cdef class Buffer:
8687
allocations.
8788
8889
Support for data interchange mechanisms are provided by DLPack.
90+
91+
Note
92+
----
93+
Pickling an IPC-enabled :class:`Buffer` embeds an
94+
:class:`~_memory.IPCBufferDescriptor`. Unpickling reconstructs the buffer
95+
by calling :meth:`from_ipc_descriptor` and therefore performs an IPC
96+
import. Do not unpickle buffers from untrusted sources.
8997
"""
9098
def __cinit__(self) -> None:
9199
self._clear()
@@ -135,6 +143,7 @@ cdef class Buffer:
135143
# pickle path cannot thread an explicit stream through. Seed the
136144
# imported buffer's deallocation with the current context's default
137145
# stream; the receiver can override via buffer.close(stream).
146+
warn_ipc_buffer_unpickle()
138147
return Buffer.from_ipc_descriptor(mr, ipc_descriptor, stream=default_stream())
139148

140149
def __reduce__(self) -> tuple[object, ...]:
@@ -187,6 +196,12 @@ cdef class Buffer:
187196
stream : :obj:`~_stream.Stream`
188197
Keyword-only. The stream used for asynchronous deallocation when
189198
the buffer is closed or garbage collected.
199+
200+
Note
201+
----
202+
The descriptor payload and ``size`` are supplied by the exporting peer
203+
and must be treated as untrusted input unless the peer is known to be
204+
cooperating.
190205
"""
191206
return _ipc.Buffer_from_ipc_descriptor(cls, mr, ipc_descriptor, stream)
192207

cuda_core/cuda/core/_memory/_device_memory_resource.pyi

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,16 @@ class DeviceMemoryResource(_MemPool):
105105
an MMR is created and registered in the receiving process. Subsequently,
106106
buffers may be serialized and transferred using ordinary :mod:`pickle`
107107
methods. The reconstruction procedure uses the registry to find the
108-
associated MMR.
108+
associated MMR. Unpickling a :class:`Buffer` performs an IPC import from
109+
the embedded descriptor; only unpickle buffers received from trusted peers.
110+
111+
Warning
112+
-------
113+
IPC descriptors and pickled buffers cross a trust boundary between
114+
cooperating same-host processes. A malicious peer can supply crafted
115+
descriptor fields. Use :meth:`Buffer.from_ipc_descriptor` only with
116+
descriptors from trusted peers, and do not unpickle buffers from
117+
untrusted sources.
109118
"""
110119

111120
def __cinit__(self, *args, **kwargs) -> None:

cuda_core/cuda/core/_memory/_device_memory_resource.pyx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,16 @@ cdef class DeviceMemoryResource(_MemPool):
130130
an MMR is created and registered in the receiving process. Subsequently,
131131
buffers may be serialized and transferred using ordinary :mod:`pickle`
132132
methods. The reconstruction procedure uses the registry to find the
133-
associated MMR.
133+
associated MMR. Unpickling a :class:`Buffer` performs an IPC import from
134+
the embedded descriptor; only unpickle buffers received from trusted peers.
135+
136+
Warning
137+
-------
138+
IPC descriptors and pickled buffers cross a trust boundary between
139+
cooperating same-host processes. A malicious peer can supply crafted
140+
descriptor fields. Use :meth:`Buffer.from_ipc_descriptor` only with
141+
descriptors from trusted peers, and do not unpickle buffers from
142+
untrusted sources.
134143
"""
135144

136145
def __cinit__(self, *args, **kwargs) -> None:

cuda_core/cuda/core/_memory/_ipc.pyi

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,14 @@ class IPCDataForMR:
3838
...
3939

4040
class IPCBufferDescriptor:
41-
"""Serializable object describing a buffer that can be shared between processes."""
41+
"""Serializable object describing a buffer that can be shared between processes.
42+
43+
Note
44+
----
45+
The payload and ``size`` fields are controlled by the exporting peer.
46+
Receivers must treat them as untrusted and import only through
47+
:meth:`Buffer.from_ipc_descriptor`.
48+
"""
4249

4350
def __init__(self, *arg, **kwargs) -> None:
4451
...

cuda_core/cuda/core/_memory/_ipc.pyx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,14 @@ cdef class IPCDataForMR:
7878

7979

8080
cdef class IPCBufferDescriptor:
81-
"""Serializable object describing a buffer that can be shared between processes."""
81+
"""Serializable object describing a buffer that can be shared between processes.
82+
83+
Note
84+
----
85+
The payload and ``size`` fields are controlled by the exporting peer.
86+
Receivers must treat them as untrusted and import only through
87+
:meth:`Buffer.from_ipc_descriptor`.
88+
"""
8289

8390
def __init__(self, *arg, **kwargs) -> None:
8491
raise RuntimeError("IPCBufferDescriptor objects cannot be instantiated directly. Please use MemoryResource APIs.")

cuda_core/cuda/core/_utils/cuda_utils.pyi

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ _keep_nvrtc_in_stub: 'nvrtc.nvrtcResult'
6060
_keep_runtime_in_stub: 'runtime.cudaError_t'
6161
ComputeCapability = namedtuple('ComputeCapability', ('major', 'minor'))
6262
_fork_warning_checked = False
63+
_ipc_pickle_warning_checked = False
6364

6465
def _check_driver_error(error: cydriver.CUresult) -> int:
6566
...
@@ -140,5 +141,11 @@ def reset_fork_warning() -> None:
140141
to check the warning behavior.
141142
"""
142143

144+
def reset_ipc_pickle_warning() -> None:
145+
"""Reset the IPC buffer unpickle warning flag for testing purposes."""
146+
147+
def warn_ipc_buffer_unpickle() -> None:
148+
"""Warn that unpickling a Buffer performs an IPC import from embedded data."""
149+
143150
def check_multiprocessing_start_method() -> None:
144151
"""Check if multiprocessing start method is 'fork' and warn if so."""

cuda_core/cuda/core/_utils/cuda_utils.pyx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,9 @@ class Transaction:
348348
# Track whether we've already warned about fork method
349349
_fork_warning_checked = False
350350
351+
# Track whether we've already warned about unpickling IPC buffers
352+
_ipc_pickle_warning_checked = False
353+
351354
352355
def reset_fork_warning() -> None:
353356
"""Reset the fork warning check flag for testing purposes.
@@ -359,6 +362,28 @@ def reset_fork_warning() -> None:
359362
_fork_warning_checked = False
360363
361364
365+
def reset_ipc_pickle_warning() -> None:
366+
"""Reset the IPC buffer unpickle warning flag for testing purposes."""
367+
global _ipc_pickle_warning_checked
368+
_ipc_pickle_warning_checked = False
369+
370+
371+
def warn_ipc_buffer_unpickle() -> None:
372+
"""Warn that unpickling a Buffer performs an IPC import from embedded data."""
373+
global _ipc_pickle_warning_checked
374+
if _ipc_pickle_warning_checked:
375+
return
376+
_ipc_pickle_warning_checked = True
377+
message = (
378+
"Unpickling a cuda.core.Buffer imports GPU memory via the embedded "
379+
"IPCBufferDescriptor. Only unpickle Buffer objects received from a "
380+
"trusted same-host peer process; malicious pickle payloads can supply "
381+
"crafted descriptors. Prefer explicit Buffer.from_ipc_descriptor only "
382+
"with descriptors from cooperating peers."
383+
)
384+
warnings.warn(message, UserWarning, stacklevel=4)
385+
386+
362387
cdef inline tuple _read_fill_ptr(const char* ptr, Py_ssize_t width):
363388
"""Extract (value, element_size) from a raw pointer of known width."""
364389
cdef unsigned int val
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Test warnings when unpickling IPC-enabled Buffer objects."""
5+
6+
import warnings
7+
8+
from cuda.core import Buffer
9+
from cuda.core._utils.cuda_utils import reset_ipc_pickle_warning, warn_ipc_buffer_unpickle
10+
11+
NBYTES = 64
12+
13+
14+
def test_warn_on_buffer_unpickle(ipc_device, ipc_memory_resource):
15+
"""Unpickling an IPC buffer warns about the trust boundary."""
16+
mr = ipc_memory_resource
17+
buf = mr.allocate(NBYTES, stream=ipc_device.default_stream)
18+
19+
with warnings.catch_warnings(record=True) as w:
20+
warnings.simplefilter("always")
21+
reset_ipc_pickle_warning()
22+
Buffer._reduce_helper(mr, buf.ipc_descriptor)
23+
24+
assert len(w) == 1, f"Expected 1 warning, got {len(w)}: {[str(x.message) for x in w]}"
25+
warning = w[0]
26+
assert warning.category is UserWarning
27+
assert "unpickl" in str(warning.message).lower()
28+
assert "ipc" in str(warning.message).lower()
29+
assert "trusted" in str(warning.message).lower()
30+
31+
32+
def test_ipc_pickle_warning_emitted_only_once(ipc_device, ipc_memory_resource):
33+
"""The unpickle trust-boundary warning is emitted at most once per process."""
34+
mr = ipc_memory_resource
35+
buf1 = mr.allocate(NBYTES, stream=ipc_device.default_stream)
36+
buf2 = mr.allocate(NBYTES, stream=ipc_device.default_stream)
37+
38+
with warnings.catch_warnings(record=True) as w:
39+
warnings.simplefilter("always")
40+
reset_ipc_pickle_warning()
41+
Buffer._reduce_helper(mr, buf1.ipc_descriptor)
42+
Buffer._reduce_helper(mr, buf2.ipc_descriptor)
43+
warn_ipc_buffer_unpickle()
44+
45+
ipc_warnings = [x for x in w if "unpickl" in str(x.message).lower()]
46+
assert len(ipc_warnings) == 1

0 commit comments

Comments
 (0)