blob: ec6cfe033905b684b3b9ffde9bae24cc60e93c2b [file]
# SPDX-License-Identifier: (GPL-2.0 OR Linux-OpenIB)
# Copyright (c) 2026 NVIDIA Corporation . All rights reserved. See COPYING file
"""
Tests for the provider-aware buffer API: ibv_alloc_buf(), ibv_free_buf(),
ibv_reg_buf_mr() and the ibv_reg_mr_ex() IBV_REG_MR_MASK_BUF path.
"""
import unittest
import errno
import resource
from pyverbs.pyverbs_error import PyverbsRDMAError
from pyverbs.pd import PD, ParentDomain, ParentDomainInitAttr
from pyverbs.mr import MR, Buf, BufMR, MREx
from pyverbs.cq import CqInitAttrEx, CQEX
import pyverbs.device as d
from pyverbs.libibverbs_enums import ibv_access_flags, ibv_atomic_cap, \
ibv_cq_init_attr_mask, ibv_wr_opcode, ibv_parent_domain_init_attr_mask, \
IBV_WC_STANDARD_FLAGS, _IBV_DEVICE_CC_DMA_BOUNCE
from tests.base import PyverbsAPITestCase, RCResources, UDResources, \
RDMATestCase
import tests.utils as u
# errnos that mean the environment cannot provide CC shared buffers / parent
# domains; the affected test is skipped rather than failed.
SKIP_ERRNOS = (errno.EOPNOTSUPP, errno.ENOENT, errno.ENODEV)
PAGE_SIZE = resource.getpagesize()
def device_has_cc_dma_bounce(ctx):
"""Whether the device reports IBV_DEVICE_CC_DMA_BOUNCE."""
return bool(ctx.query_device_ex().device_cap_flags_ex &
_IBV_DEVICE_CC_DMA_BOUNCE)
def make_cc_pd(ctx):
"""
Allocate a base PD and a CC PD opting in to unprotected/shared memory for
CoCo guests. Returns a (base_pd, cc_pd) tuple; the base PD must outlive the
CC PD. Skips if CC PDs are unsupported.
"""
base_pd = PD(ctx)
attr = ParentDomainInitAttr(
pd=base_pd,
comp_mask=ibv_parent_domain_init_attr_mask.
IBV_PARENT_DOMAIN_INIT_ATTR_ALLOW_CC_UNPROTECTED_ALLOC)
try:
pd = ParentDomain(ctx, attr=attr)
except PyverbsRDMAError as ex:
base_pd.close()
if ex.error_code in SKIP_ERRNOS:
raise unittest.SkipTest('CC PD is not supported')
raise
return base_pd, pd
def alloc_buf(pd, size):
"""
Allocate a buffer of ibv_buf type, skipping if the provider
does not support it
"""
try:
return Buf(pd, size)
except PyverbsRDMAError as ex:
if ex.error_code in SKIP_ERRNOS:
raise unittest.SkipTest('ibv_alloc_buf() is not supported')
raise
def register_buf_mr(pd, buf, length, access, offset=0, via_reg_mr_ex=False):
"""
Register (a subrange of) buf, either through ibv_reg_buf_mr() or, when
via_reg_mr_ex is set, through the ibv_reg_mr_ex() IBV_REG_MR_MASK_BUF path.
"""
try:
if via_reg_mr_ex:
return MREx(pd, length=length, access=access, buf=buf,
address=buf.addr + offset)
return BufMR(pd, buf, length, access, offset=offset)
except PyverbsRDMAError as ex:
if ex.error_code in SKIP_ERRNOS:
raise unittest.SkipTest('Buffer MR registration is not supported')
raise
def init_buf_resource(res, cc, via_reg_mr_ex, mr_access, buf_size, mr_offset):
"""Store the buffer parameters before the base resource init runs."""
res.cc = cc
res.via_reg_mr_ex = via_reg_mr_ex
res.mr_access = mr_access
res.buf_size = buf_size
res.mr_offset = mr_offset
res.base_pd = None
res.data_buf = None
def create_pd(res):
"""Create the resource's PD: a plain PD or a CC PD."""
if res.cc:
res.base_pd, res.pd = make_cc_pd(res.ctx)
else:
# A plain PD registers private memory, which a DMA-bounce device
# rejects; that device is exercised by the cc=True resources.
if device_has_cc_dma_bounce(res.ctx):
raise unittest.SkipTest('Plain-memory registration is rejected on '
'a DMA-bounce device')
res.pd = PD(res.ctx)
def create_cq(res):
"""Create the resource's extended CQ, bound to the CC PD for cc."""
comp_mask = ibv_cq_init_attr_mask.IBV_CQ_INIT_ATTR_MASK_FLAGS
if res.cc:
comp_mask |= ibv_cq_init_attr_mask.IBV_CQ_INIT_ATTR_MASK_PD
cqia = CqInitAttrEx(cqe=res.num_msgs, wc_flags=IBV_WC_STANDARD_FLAGS,
parent_domain=res.pd if res.cc else None,
comp_mask=comp_mask)
try:
res.cq = CQEX(res.ctx, cqia)
except PyverbsRDMAError as ex:
if ex.error_code in SKIP_ERRNOS:
raise unittest.SkipTest('Extended CQ is not supported')
raise
def create_buf_mr(res, mr_len, buf_size, offset):
"""Allocate an ibv_buf and register (a subrange of) it as the MR."""
if buf_size is None:
buf_size = mr_len + offset
res.data_buf = alloc_buf(res.pd, buf_size)
res.mr = register_buf_mr(res.pd, res.data_buf, mr_len, res.mr_access,
offset=offset, via_reg_mr_ex=res.via_reg_mr_ex)
class BufRC(RCResources):
"""RC resources whose data buffer is allocated with ibv_alloc_buf()."""
def __init__(self, *args, cc=False, via_reg_mr_ex=False,
mr_access=ibv_access_flags.IBV_ACCESS_LOCAL_WRITE,
buf_size=None, mr_offset=0, **kwargs):
init_buf_resource(self, cc, via_reg_mr_ex, mr_access, buf_size,
mr_offset)
super().__init__(*args, **kwargs)
def create_pd(self):
create_pd(self)
def create_cq(self):
create_cq(self)
def create_mr(self):
create_buf_mr(self, self.msg_size, self.buf_size, self.mr_offset)
def create_qp_attr(self):
attr = super().create_qp_attr()
attr.qp_access_flags = self.mr_access
return attr
class BufUD(UDResources):
"""UD resources whose data buffer is allocated with ibv_alloc_buf()."""
def __init__(self, *args, cc=False, via_reg_mr_ex=False,
mr_access=ibv_access_flags.IBV_ACCESS_LOCAL_WRITE,
buf_size=None, mr_offset=0, **kwargs):
init_buf_resource(self, cc, via_reg_mr_ex, mr_access, buf_size,
mr_offset)
super().__init__(*args, **kwargs)
def create_pd(self):
create_pd(self)
def create_cq(self):
create_cq(self)
def create_mr(self):
# UD prepends a GRH on receive, so the buffer needs room for it.
mr_len = self.msg_size + self.GRH_SIZE
create_buf_mr(self, mr_len, self.buf_size, self.mr_offset)
class BufAPITest(PyverbsAPITestCase):
"""Single-node API tests for ibv_alloc_buf()/ibv_reg_buf_mr()."""
def get_pd(self, cc=False):
"""Create a PD, or a CC PD when cc is set."""
if cc:
_, pd = make_cc_pd(self.ctx)
return pd
if device_has_cc_dma_bounce(self.ctx):
raise unittest.SkipTest('Plain-memory registration is rejected on '
'a DMA-bounce device')
return PD(self.ctx)
def get_buf(self, pd, size):
return alloc_buf(pd, size)
def get_mr(self, pd, buf, length, offset=0, via_reg_mr_ex=False):
return register_buf_mr(pd, buf, length,
ibv_access_flags.IBV_ACCESS_LOCAL_WRITE,
offset=offset, via_reg_mr_ex=via_reg_mr_ex)
def check_multiple_mrs_one_buf(self, pd, via_reg_mr_ex=False):
"""Register two disjoint subranges of one buffer and access them."""
buf = self.get_buf(pd, 2 * PAGE_SIZE)
mr1 = self.get_mr(pd, buf, PAGE_SIZE, offset=0,
via_reg_mr_ex=via_reg_mr_ex)
mr2 = self.get_mr(pd, buf, PAGE_SIZE, offset=PAGE_SIZE,
via_reg_mr_ex=via_reg_mr_ex)
self.assertNotEqual(mr1.lkey, mr2.lkey,
'MR lkeys for disjoint subranges must differ')
self.assertEqual(mr1.buf, buf.addr,
'MR1 address does not match buffer start')
self.assertEqual(mr2.buf, buf.addr + PAGE_SIZE,
'MR2 address does not match its subrange start')
mr1.write('a' * PAGE_SIZE, PAGE_SIZE)
mr2.write('b' * PAGE_SIZE, PAGE_SIZE)
self.assertEqual(mr1.read(PAGE_SIZE, 0), b'a' * PAGE_SIZE,
'MR1 readback does not match written data')
self.assertEqual(mr2.read(PAGE_SIZE, 0), b'b' * PAGE_SIZE,
'MR2 readback does not match written data')
def test_multiple_mrs_one_buf_plain_pd_reg_buf_mr(self):
"""Plain PD: registered with ibv_reg_buf_mr()."""
self.check_multiple_mrs_one_buf(self.get_pd())
def test_multiple_mrs_one_buf_plain_pd_reg_mr_ex(self):
"""Plain PD: registered with the ibv_reg_mr_ex() MASK_BUF path."""
self.check_multiple_mrs_one_buf(self.get_pd(), via_reg_mr_ex=True)
def test_multiple_mrs_one_buf_cc_pd_reg_buf_mr(self):
"""CC PD: registered with ibv_reg_buf_mr()."""
self.check_multiple_mrs_one_buf(self.get_pd(cc=True))
def check_reg_wrong_allocating_pd_fails(self, pd, other_pd):
"""Registering a buffer with a non-allocating PD must fail."""
buf = self.get_buf(pd, PAGE_SIZE)
with self.assertRaises(PyverbsRDMAError) as cm:
register_buf_mr(other_pd, buf, PAGE_SIZE,
ibv_access_flags.IBV_ACCESS_LOCAL_WRITE)
self.assertEqual(cm.exception.error_code, errno.EINVAL,
'Registering a buffer with a non-allocating PD '
'must fail with EINVAL')
def test_buf_reg_wrong_allocating_pd_fails_plain_pd(self):
"""Plain PDs: a non-allocating PD is rejected."""
self.check_reg_wrong_allocating_pd_fails(self.get_pd(), self.get_pd())
def test_buf_reg_wrong_allocating_pd_fails_cc_pd(self):
"""CC PDs: a non-allocating PD is rejected."""
self.check_reg_wrong_allocating_pd_fails(self.get_pd(cc=True),
self.get_pd(cc=True))
def check_reg_length_exceeds_buffer_fails(self, pd):
"""Registering a length larger than the buffer must fail."""
buf = self.get_buf(pd, PAGE_SIZE)
with self.assertRaises(PyverbsRDMAError) as cm:
register_buf_mr(pd, buf, 2 * PAGE_SIZE,
ibv_access_flags.IBV_ACCESS_LOCAL_WRITE)
self.assertEqual(cm.exception.error_code, errno.EINVAL,
'Registering a length larger than the buffer '
'must fail with EINVAL')
def test_buf_reg_length_exceeds_buffer_fails_plain_pd(self):
"""Plain PD: a too-large length is rejected."""
self.check_reg_length_exceeds_buffer_fails(self.get_pd())
def test_buf_reg_length_exceeds_buffer_fails_cc_pd(self):
"""CC PD: a too-large length is rejected."""
self.check_reg_length_exceeds_buffer_fails(self.get_pd(cc=True))
def check_reg_offset_length_exceeds_buffer_fails(self, pd):
"""Registering offset + length past the buffer must fail."""
buf = self.get_buf(pd, PAGE_SIZE)
with self.assertRaises(PyverbsRDMAError) as cm:
register_buf_mr(pd, buf, PAGE_SIZE,
ibv_access_flags.IBV_ACCESS_LOCAL_WRITE,
offset=PAGE_SIZE)
self.assertEqual(cm.exception.error_code, errno.EINVAL,
'Registering past the buffer end via offset '
'must fail with EINVAL')
def test_buf_reg_offset_length_exceeds_buffer_fails_plain_pd(self):
"""Plain PD: an out-of-range offset+length is rejected."""
self.check_reg_offset_length_exceeds_buffer_fails(self.get_pd())
def test_buf_reg_offset_length_exceeds_buffer_fails_cc_pd(self):
"""CC PD: an out-of-range offset+length is rejected."""
self.check_reg_offset_length_exceeds_buffer_fails(self.get_pd(cc=True))
def test_plain_mr_rejected_on_bounce_device(self):
"""DMA-bounce device: a plain ibv_reg_mr() is rejected."""
if not device_has_cc_dma_bounce(self.ctx):
raise unittest.SkipTest('Device does not report CC_DMA_BOUNCE')
with PD(self.ctx) as pd:
with self.assertRaises(
PyverbsRDMAError,
msg='Plain ibv_reg_mr() must be rejected on a '
'DMA-bounce device'):
MR(pd, PAGE_SIZE, ibv_access_flags.IBV_ACCESS_LOCAL_WRITE)
class BufTrafficTest(RDMATestCase):
"""RC/UD traffic over ibv_alloc_buf() data buffers."""
def test_buf_rc_send_cc_pd_reg_buf_mr(self):
"""CC PD: RC send/recv over a buffer MR."""
self.create_players(BufRC, cc=True)
u.traffic(**self.traffic_args, is_cq_ex=True)
def test_buf_rc_send_large_msg_cc_pd_reg_buf_mr(self):
"""CC PD: RC send/recv with a multi-page buffer MR."""
self.create_players(BufRC, cc=True, msg_size=16384)
u.traffic(**self.traffic_args, is_cq_ex=True)
def test_buf_rc_send_mr_subrange_cc_pd_reg_buf_mr(self):
"""CC PD: RC send/recv over a buffer-subrange MR."""
self.create_players(BufRC, cc=True, mr_offset=PAGE_SIZE,
buf_size=PAGE_SIZE + 16384, msg_size=8192)
u.traffic(**self.traffic_args, is_cq_ex=True)
def test_buf_rc_rdma_write_imm_cc_pd_reg_buf_mr(self):
"""CC PD: RC RDMA write-with-immediate into a buffer MR."""
access = (ibv_access_flags.IBV_ACCESS_LOCAL_WRITE |
ibv_access_flags.IBV_ACCESS_REMOTE_WRITE)
self.create_players(BufRC, cc=True, mr_access=access)
u.traffic(**self.traffic_args, is_cq_ex=True,
send_op=ibv_wr_opcode.IBV_WR_RDMA_WRITE_WITH_IMM)
def test_buf_rc_rdma_read_cc_pd_reg_buf_mr(self):
"""CC PD: RC RDMA read from a buffer MR."""
access = (ibv_access_flags.IBV_ACCESS_LOCAL_WRITE |
ibv_access_flags.IBV_ACCESS_REMOTE_READ)
self.create_players(BufRC, cc=True, mr_access=access)
u.rdma_traffic(**self.traffic_args, is_cq_ex=True,
send_op=ibv_wr_opcode.IBV_WR_RDMA_READ)
def test_buf_rc_atomic_fetch_add_cc_pd_reg_buf_mr(self):
"""CC PD: RC atomic fetch&add on a buffer MR."""
with d.Context(name=self.dev_name) as ctx:
atomic_caps = ctx.query_device().atomic_caps
if atomic_caps == ibv_atomic_cap.IBV_ATOMIC_NONE:
raise unittest.SkipTest('Atomic operations are not supported')
access = (ibv_access_flags.IBV_ACCESS_LOCAL_WRITE |
ibv_access_flags.IBV_ACCESS_REMOTE_WRITE |
ibv_access_flags.IBV_ACCESS_REMOTE_ATOMIC)
self.create_players(BufRC, cc=True, mr_access=access, msg_size=8)
u.atomic_traffic(**self.traffic_args, is_cq_ex=True,
send_op=ibv_wr_opcode.IBV_WR_ATOMIC_FETCH_AND_ADD)
def test_buf_ud_send_cc_pd_reg_buf_mr(self):
"""CC PD: UD send/recv over a buffer MR."""
self.create_players(BufUD, cc=True)
u.traffic(**self.traffic_args, is_cq_ex=True)
def test_buf_rc_send_plain_pd_reg_buf_mr(self):
"""Plain PD: RC send/recv over a buffer MR."""
self.create_players(BufRC, cc=False)
u.traffic(**self.traffic_args, is_cq_ex=True)
def test_buf_rc_send_cc_pd_reg_mr_ex(self):
"""CC PD: RC send/recv via ibv_reg_mr_ex()."""
self.create_players(BufRC, cc=True, via_reg_mr_ex=True)
u.traffic(**self.traffic_args, is_cq_ex=True)