From 7d0b5b471613bad6178354a18598a8180e61d520 Mon Sep 17 00:00:00 2001 From: Martin Belanger Date: Mon, 20 Apr 2026 12:34:35 -0400 Subject: [PATCH 1/3] swig: fix bugs introduced in libnvmf_context refactor Three bugs were introduced when libnvmf_context replaced the old libnvme_fabrics_config parameter in the connect/create flow: 1. The %pythonappend GC guard for connect() had a stale signature that still included the removed fctx parameter. SWIG silently ignores a mismatched %pythonappend, so self.__host = h never ran, leaving a latent use-after-free where Python could collect the host object while the ctrl was still alive. 2. The duplicate_connect check in connect() had an inverted condition (missing !) and belonged in the library, not the binding. Move it to libnvmf_add_ctrl() in fabrics.c where it applies uniformly to all callers, C and Python alike. 3. fctx_set_fabrics_config was missing its %rename directive, exposing it to Python as fctx.fctx_set_fabrics_config() instead of fctx.set_fabrics_config() like all other libnvmf_context methods. Signed-off-by: Martin Belanger Assisted-by: Claude Sonnet 4.6 --- libnvme/libnvme/nvme.i | 11 ++--------- libnvme/src/nvme/fabrics.c | 4 ++++ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/libnvme/libnvme/nvme.i b/libnvme/libnvme/nvme.i index b2641f548d..3b0cda8b09 100644 --- a/libnvme/libnvme/nvme.i +++ b/libnvme/libnvme/nvme.i @@ -443,6 +443,7 @@ struct libnvme_ns { %rename(set_connection) libnvmf_context::fctx_set_connection; %rename(set_persistent) libnvmf_context::fctx_set_persistent; %rename(set_device) libnvmf_context::fctx_set_device; +%rename(set_fabrics_config) libnvmf_context::fctx_set_fabrics_config; struct libnvmf_context {}; @@ -725,8 +726,7 @@ struct libnvmf_context {}; } %}; -%pythonappend libnvme_ctrl::connect(struct libnvme_host *h, - struct libnvmf_context *fctx) { +%pythonappend libnvme_ctrl::connect(struct libnvme_host *h) { self.__host = h # Keep a reference to parent to ensure ctrl obj gets GCed before host} %pythonappend libnvme_ctrl::init(struct libnvme_host *h, int instance) { self.__host = h # Keep a reference to parent to ensure ctrl obj gets GCed before host} @@ -756,13 +756,6 @@ struct libnvmf_context {}; void connect(struct libnvme_host *h) { int ret; - const char *dev; - - dev = libnvme_ctrl_get_name($self); - if (dev && $self->cfg.duplicate_connect) { - connect_err = -ENVME_CONNECT_ALREADY; - return; - } Py_BEGIN_ALLOW_THREADS /* Release Python GIL */ ret = libnvmf_add_ctrl(h, $self); diff --git a/libnvme/src/nvme/fabrics.c b/libnvme/src/nvme/fabrics.c index 2512afa7b8..5d91a5b216 100644 --- a/libnvme/src/nvme/fabrics.c +++ b/libnvme/src/nvme/fabrics.c @@ -1102,6 +1102,10 @@ __public int libnvmf_add_ctrl(libnvme_host_t h, libnvme_ctrl_t c) __cleanup_free char *argstr = NULL; int ret; + /* Are duplicate connections allowed on existing controller */ + if (libnvme_ctrl_get_name(c) && !c->cfg.duplicate_connect) + return -ENVME_CONNECT_ALREADY; + /* apply configuration from config file (JSON) */ s = libnvme_lookup_subsystem(h, NULL, libnvme_ctrl_get_subsysnqn(c)); if (s) { From ff76cc9465151277de490d68ca79f5135a6e53de Mon Sep 17 00:00:00 2001 From: Martin Belanger Date: Mon, 20 Apr 2026 16:30:34 -0400 Subject: [PATCH 2/3] swig: replace libnvmf_context with dict-based ctrl constructor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The libnvmf_context object was a leaky abstraction: Python callers had to create it, configure it, pass it to ctrl(), then discard it — despite never needing it after construction. This commit restores the ergonomics that were lost when the C API switched from libnvme_fabrics_config to libnvmf_context. The public header declares libnvmf_context as an opaque forward declaration, which caused SWIG to treat it as an opaque pointer type and override any user-defined %typemap. Fix this by providing an empty struct body in the SWIG parsed section, which gives SWIG a complete type and restores normal typemap precedence without modifying any C header. The %typemap(in) / %typemap(freearg) pair for struct libnvmf_context * now handles the full fctx lifecycle transparently: the typemap creates the fctx from the Python dict (using arg1 as the global context), the constructor body calls libnvmf_create_ctrl(), and freearg frees the fctx. The typemap is reusable for any future function that takes struct libnvmf_context *. The dict supports all configurable fields: connection identity (subsysnqn, transport, traddr, trsvcid, host_traddr, host_iface), the full libnvme_fabrics_config (via libnvme_fabrics_config_set_* accessors and libnvmf_context_get_fabrics_config), host identity (hostnqn, hostid), crypto and TLS (hostkey, ctrlkey, keyring, tls_key, tls_key_identity), and persistence. All context-level fields are set via their public API setters. An autodoc string on the constructor documents every supported key so that help(nvme.ctrl) is the complete reference. Tests are updated to use the new dict-based API. Signed-off-by: Martin Belanger Assisted-by: Claude Sonnet 4.6 --- libnvme/libnvme/nvme.i | 381 ++++++++++++++++------- libnvme/libnvme/tests/create-ctrl-obj.py | 17 +- libnvme/libnvme/tests/gc.py | 12 +- libnvme/libnvme/tests/test-objects.py | 70 ++--- 4 files changed, 304 insertions(+), 176 deletions(-) diff --git a/libnvme/libnvme/nvme.i b/libnvme/libnvme/nvme.i index 3b0cda8b09..88520aea5f 100644 --- a/libnvme/libnvme/nvme.i +++ b/libnvme/libnvme/nvme.i @@ -19,12 +19,11 @@ %allowexception; -%rename(global_ctx) libnvme_global_ctx; -%rename(host) libnvme_host; -%rename(ctrl) libnvme_ctrl; -%rename(subsystem) libnvme_subsystem; -%rename(ns) libnvme_ns; -%rename(fabrics_context) libnvmf_context; +%rename(global_ctx) libnvme_global_ctx; +%rename(host) libnvme_host; +%rename(ctrl) libnvme_ctrl; +%rename(subsystem) libnvme_subsystem; +%rename(ns) libnvme_ns; %{ #include @@ -52,10 +51,213 @@ free(val); return obj; } + + static const char *dict_get_str(PyObject *dict, const char *key) + { + PyObject *val = PyDict_GetItemString(dict, key); + + if (!val || val == Py_None) + return NULL; + return PyUnicode_AsUTF8(val); + } + + static int set_fctx_from_dict(struct libnvmf_context *fctx, PyObject *dict) + { + struct libnvme_fabrics_config *cfg; + const char *subsysnqn, *transport; + const char *hostnqn = NULL, *hostid = NULL; + const char *hostkey = NULL, *ctrlkey = NULL; + const char *keyring = NULL, *tls_key = NULL, *tls_key_identity = NULL; + bool persistent = false; + bool has_persistent = false; + Py_ssize_t pos = 0; + PyObject *key, *value; + + subsysnqn = dict_get_str(dict, "subsysnqn"); + transport = dict_get_str(dict, "transport"); + + if (!subsysnqn || !transport) { + PyErr_SetString(PyExc_KeyError, + "'subsysnqn' and 'transport' are required"); + return -1; + } + + libnvmf_context_set_connection(fctx, subsysnqn, transport, + dict_get_str(dict, "traddr"), + dict_get_str(dict, "trsvcid"), + dict_get_str(dict, "host_traddr"), + dict_get_str(dict, "host_iface")); + + cfg = libnvmf_context_get_fabrics_config(fctx); + + while (PyDict_Next(dict, &pos, &key, &value)) { + if (!PyUnicode_CompareWithASCIIString(key, "queue_size")) { + libnvme_fabrics_config_set_queue_size(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "nr_io_queues")) { + libnvme_fabrics_config_set_nr_io_queues(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "reconnect_delay")) { + libnvme_fabrics_config_set_reconnect_delay(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "ctrl_loss_tmo")) { + libnvme_fabrics_config_set_ctrl_loss_tmo(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "fast_io_fail_tmo")) { + libnvme_fabrics_config_set_fast_io_fail_tmo(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "keep_alive_tmo")) { + libnvme_fabrics_config_set_keep_alive_tmo(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "nr_write_queues")) { + libnvme_fabrics_config_set_nr_write_queues(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "nr_poll_queues")) { + libnvme_fabrics_config_set_nr_poll_queues(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "tos")) { + libnvme_fabrics_config_set_tos(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "keyring_id")) { + libnvme_fabrics_config_set_keyring_id(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "tls_key_id")) { + libnvme_fabrics_config_set_tls_key_id(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "tls_configured_key_id")) { + libnvme_fabrics_config_set_tls_configured_key_id(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "duplicate_connect")) { + libnvme_fabrics_config_set_duplicate_connect(cfg, + PyObject_IsTrue(value) ? true : false); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "disable_sqflow")) { + libnvme_fabrics_config_set_disable_sqflow(cfg, + PyObject_IsTrue(value) ? true : false); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "hdr_digest")) { + libnvme_fabrics_config_set_hdr_digest(cfg, + PyObject_IsTrue(value) ? true : false); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "data_digest")) { + libnvme_fabrics_config_set_data_digest(cfg, + PyObject_IsTrue(value) ? true : false); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "tls")) { + libnvme_fabrics_config_set_tls(cfg, + PyObject_IsTrue(value) ? true : false); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "concat")) { + libnvme_fabrics_config_set_concat(cfg, + PyObject_IsTrue(value) ? true : false); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "hostnqn")) { + hostnqn = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "hostid")) { + hostid = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "hostkey")) { + hostkey = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "ctrlkey")) { + ctrlkey = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "keyring")) { + keyring = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "tls_key")) { + tls_key = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "tls_key_identity")) { + tls_key_identity = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "persistent")) { + persistent = PyObject_IsTrue(value) ? true : false; + has_persistent = true; + continue; + } + /* Unknown keys are silently ignored */ + } + + if (hostnqn || hostid) + libnvmf_context_set_hostnqn(fctx, hostnqn, hostid); + if (hostkey || ctrlkey || keyring || tls_key || tls_key_identity) + libnvmf_context_set_crypto(fctx, hostkey, ctrlkey, + keyring, tls_key, + tls_key_identity); + if (has_persistent) + libnvmf_context_set_persistent(fctx, persistent); + + return 0; + } %} PyObject *read_hostnqn(); PyObject *read_hostid(); +/* libnvmf_context is declared as an opaque forward declaration in the public + * header fabrics.h. SWIG treats any type it knows only as an incomplete type + * (no struct body) as an opaque pointer, which causes SWIG's own built-in + * opaque-pointer typemap to take precedence over any user-defined %typemap. + * Providing an empty struct body here gives SWIG a complete type, which + * restores normal typemap precedence rules. The actual fields are in + * private-fabrics.h, which is included in the %{%} block above and is + * therefore visible to all generated C code — just not to SWIG's parser. + */ +struct libnvmf_context {}; + +/* Convert a Python dict to a struct libnvmf_context * automatically. + * arg1 is the libnvme_global_ctx * (first argument of the enclosing function). + * The context is created here and freed by %typemap(freearg) after the call. + */ +%typemap(in) struct libnvmf_context * (struct libnvmf_context *temp = NULL) { + if (!PyDict_Check($input)) { + PyErr_SetString(PyExc_TypeError, + "expected a dict for fabrics context argument"); + SWIG_fail; + } + if (libnvmf_context_create(arg1, NULL, NULL, NULL, NULL, &temp)) { + PyErr_SetString(PyExc_RuntimeError, + "failed to create fabrics context"); + SWIG_fail; + } + if (set_fctx_from_dict(temp, $input)) { + libnvmf_context_free(temp); + temp = NULL; + SWIG_fail; + } + $1 = temp; +} + +%typemap(freearg) struct libnvmf_context * { + libnvmf_context_free($1); +} + %exception libnvme_ctrl::connect { connect_err = 0; $action /* $action sets connect_err to non-zero value on failure */ @@ -432,114 +634,6 @@ struct libnvme_ns { uint8_t uuid[16]; }; -/* - * %rename directives give the %extend methods Python-friendly names while - * using distinct C-level names (fctx_*) that do not collide with the public - * libnvmf_context_* API declarations in fabrics.h. Without this, SWIG would - * emit SWIGINTERN libnvmf_context_set_hostnqn() which clashes with the - * non-static extern of the same name. - */ -%rename(set_hostnqn) libnvmf_context::fctx_set_hostnqn; -%rename(set_connection) libnvmf_context::fctx_set_connection; -%rename(set_persistent) libnvmf_context::fctx_set_persistent; -%rename(set_device) libnvmf_context::fctx_set_device; -%rename(set_fabrics_config) libnvmf_context::fctx_set_fabrics_config; - -struct libnvmf_context {}; - -%extend libnvmf_context { - libnvmf_context(struct libnvme_global_ctx *ctx) { - struct libnvmf_context *fctx; - int err; - - err = libnvmf_context_create(ctx, NULL, NULL, NULL, NULL, &fctx); - if (err) - return NULL; - - return fctx; - } - ~libnvmf_context() { - libnvmf_context_free($self); - } - int fctx_set_hostnqn(const char *hostnqn, const char *hostid = NULL) { - return libnvmf_context_set_hostnqn($self, hostnqn, hostid); - } - int fctx_set_connection(const char *subsysnqn, const char *transport, - const char *traddr = NULL, const char *trsvcid = NULL, - const char *host_traddr = NULL, - const char *host_iface = NULL) { - return libnvmf_context_set_connection($self, subsysnqn, transport, - traddr, trsvcid, - host_traddr, host_iface); - } - int fctx_set_persistent(bool persistent) { - return libnvmf_context_set_persistent($self, persistent); - } - int fctx_set_device(const char *device) { - return libnvmf_context_set_device($self, device); - } - void fctx_set_fabrics_config(PyObject *dict) { - Py_ssize_t pos = 0; - PyObject *key, *value; - - if (!PyDict_Check(dict)) { - PyErr_SetString(PyExc_TypeError, - "set_fabrics_config: argument must be a dict"); - return; - } - - while (PyDict_Next(dict, &pos, &key, &value)) { - if (!PyUnicode_CompareWithASCIIString(key, "nr_io_queues")) { - $self->cfg.nr_io_queues = PyLong_AsLong(value); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "reconnect_delay")) { - $self->cfg.reconnect_delay = PyLong_AsLong(value); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "ctrl_loss_tmo")) { - $self->cfg.ctrl_loss_tmo = PyLong_AsLong(value); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "keep_alive_tmo")) { - $self->cfg.keep_alive_tmo = PyLong_AsLong(value); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "nr_write_queues")) { - $self->cfg.nr_write_queues = PyLong_AsLong(value); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "nr_poll_queues")) { - $self->cfg.nr_poll_queues = PyLong_AsLong(value); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "tos")) { - $self->cfg.tos = PyLong_AsLong(value); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "duplicate_connect")) { - $self->cfg.duplicate_connect = - PyObject_IsTrue(value) ? true : false; - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "disable_sqflow")) { - $self->cfg.disable_sqflow = - PyObject_IsTrue(value) ? true : false; - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "hdr_digest")) { - $self->cfg.hdr_digest = - PyObject_IsTrue(value) ? true : false; - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "data_digest")) { - $self->cfg.data_digest = - PyObject_IsTrue(value) ? true : false; - continue; - } - } - } -}; %extend libnvme_global_ctx { libnvme_global_ctx(const char *config_file = NULL) { @@ -731,11 +825,64 @@ struct libnvmf_context {}; %pythonappend libnvme_ctrl::init(struct libnvme_host *h, int instance) { self.__host = h # Keep a reference to parent to ensure ctrl obj gets GCed before host} %extend libnvme_ctrl { - libnvme_ctrl(struct libnvme_global_ctx *ctx, - struct libnvmf_context *fctx) { + %feature("autodoc", "__init__(ctrl self, global_ctx ctx, dict fctx) -> ctrl\n" + "\n" + "Create a new NVMe-oF controller object.\n" + "\n" + " params dict keys:\n" + "\n" + " Required:\n" + " subsysnqn (str) -- Subsystem NQN\n" + " transport (str) -- Transport type: 'tcp', 'rdma', 'loop', 'fc'\n" + "\n" + " Connection (optional):\n" + " traddr (str) -- Transport address\n" + " trsvcid (str) -- Service ID (port number)\n" + " host_traddr (str) -- Host transport address\n" + " host_iface (str) -- Host network interface\n" + "\n" + " Fabrics config (optional):\n" + " queue_size (int) -- IO queue entries\n" + " nr_io_queues (int) -- Number of IO queues\n" + " reconnect_delay (int) -- Reconnect interval in seconds\n" + " ctrl_loss_tmo (int) -- Controller loss timeout in seconds\n" + " fast_io_fail_tmo (int) -- Fast I/O fail timeout in seconds\n" + " keep_alive_tmo (int) -- Keep-alive timeout in seconds\n" + " nr_write_queues (int) -- Queues reserved for writes only\n" + " nr_poll_queues (int) -- Queues reserved for polling\n" + " tos (int) -- Type of service\n" + " keyring_id (int) -- Keyring ID for key lookup\n" + " tls_key_id (int) -- TLS PSK key ID\n" + " tls_configured_key_id (int) -- TLS PSK key ID for connect command\n" + " duplicate_connect (bool) -- Allow duplicate connections\n" + " disable_sqflow (bool) -- Disable SQ flow control\n" + " hdr_digest (bool) -- Header digest (TCP only)\n" + " data_digest (bool) -- Data digest (TCP only)\n" + " tls (bool) -- Enable TLS (TCP only)\n" + " concat (bool) -- Secure concatenation (TCP only)\n" + "\n" + " Host identity (optional):\n" + " hostnqn (str) -- Host NQN, overrides the system-wide default\n" + " hostid (str) -- Host ID, overrides the system-wide default\n" + "\n" + " Authentication and TLS (optional):\n" + " hostkey (str) -- Host DH-HMAC-CHAP key\n" + " ctrlkey (str) -- Controller DH-HMAC-CHAP key\n" + " keyring (str) -- Keyring identifier\n" + " tls_key (str) -- TLS key, or 'pin:' for PIN-derived key\n" + " tls_key_identity (str) -- TLS key identity string\n" + "\n" + " Persistence (optional):\n" + " persistent (bool) -- Keep connection alive after process exit\n" + ) libnvme_ctrl; + libnvme_ctrl(struct libnvme_global_ctx *ctx, struct libnvmf_context *fctx) { struct libnvme_ctrl *c; - if (libnvmf_create_ctrl(ctx, fctx, &c)) + + if (libnvmf_create_ctrl(ctx, fctx, &c)) { + PyErr_SetString(PyExc_RuntimeError, + "failed to create ctrl"); return NULL; + } return c; } ~libnvme_ctrl() { diff --git a/libnvme/libnvme/tests/create-ctrl-obj.py b/libnvme/libnvme/tests/create-ctrl-obj.py index 9112eae964..59a05f15c5 100755 --- a/libnvme/libnvme/tests/create-ctrl-obj.py +++ b/libnvme/libnvme/tests/create-ctrl-obj.py @@ -4,15 +4,12 @@ from libnvme import nvme -ctx = nvme.global_ctx() +ctx = nvme.global_ctx() ctx.log_level('debug') -fctx = nvme.fabrics_context(ctx) -fctx.set_connection( - subsysnqn=nvme.NVME_DISC_SUBSYS_NAME, - transport='loop', - traddr='127.0.0.1', - trsvcid='8009', -) - -ctrl = nvme.ctrl(ctx, fctx) +ctrl = nvme.ctrl(ctx, { + 'subsysnqn': nvme.NVME_DISC_SUBSYS_NAME, + 'transport': 'loop', + 'traddr': '127.0.0.1', + 'trsvcid': '8009', +}) diff --git a/libnvme/libnvme/tests/gc.py b/libnvme/libnvme/tests/gc.py index c453fdec93..682074b9e8 100755 --- a/libnvme/libnvme/tests/gc.py +++ b/libnvme/libnvme/tests/gc.py @@ -12,16 +12,12 @@ host = nvme.host(ctx) print(f'host: {host}') -fctx = nvme.fabrics_context(ctx) -fctx.set_connection( - subsysnqn=nvme.NVME_DISC_SUBSYS_NAME, - transport='loop', -) -print(f'fctx: {fctx}') - ctrls = [] for i in range(10): - ctrl = nvme.ctrl(ctx, fctx) + ctrl = nvme.ctrl(ctx, { + 'subsysnqn': nvme.NVME_DISC_SUBSYS_NAME, + 'transport': 'loop', + }) ctrls.append(ctrl) print(f'ctrl {i}: {ctrl}') diff --git a/libnvme/libnvme/tests/test-objects.py b/libnvme/libnvme/tests/test-objects.py index 9fafe0fe00..0fc2a885c4 100644 --- a/libnvme/libnvme/tests/test-objects.py +++ b/libnvme/libnvme/tests/test-objects.py @@ -118,26 +118,22 @@ def tearDown(self): gc.collect() def _make_loop_ctrl(self): - fctx = nvme.fabrics_context(self.ctx) - fctx.set_connection( - subsysnqn=self.subsysnqn, - transport='loop' - ) - return nvme.ctrl(self.ctx, fctx) + return nvme.ctrl(self.ctx, { + 'subsysnqn': self.subsysnqn, + 'transport': 'loop', + }) def test_creation_loop_transport(self): ctrl = self._make_loop_ctrl() self.assertIsNotNone(ctrl) def test_creation_tcp_transport_with_traddr(self): - fctx = nvme.fabrics_context(self.ctx) - fctx.set_connection( - subsysnqn=self.subsysnqn, - transport='tcp', - traddr='192.168.1.1', - trsvcid='4420' - ) - ctrl = nvme.ctrl(self.ctx, fctx) + ctrl = nvme.ctrl(self.ctx, { + 'subsysnqn': self.subsysnqn, + 'transport': 'tcp', + 'traddr': '192.168.1.1', + 'trsvcid': '4420', + }) self.assertIsNotNone(ctrl) def test_transport_property(self): @@ -149,24 +145,20 @@ def test_subsysnqn_property(self): self.assertEqual(ctrl.subsysnqn, self.subsysnqn) def test_traddr_property(self): - fctx = nvme.fabrics_context(self.ctx) - fctx.set_connection( - subsysnqn=self.subsysnqn, - transport='tcp', - traddr='10.0.0.1', - ) - ctrl = nvme.ctrl(self.ctx, fctx) + ctrl = nvme.ctrl(self.ctx, { + 'subsysnqn': self.subsysnqn, + 'transport': 'tcp', + 'traddr': '10.0.0.1', + }) self.assertEqual(ctrl.traddr, '10.0.0.1') def test_trsvcid_property(self): - fctx = nvme.fabrics_context(self.ctx) - fctx.set_connection( - subsysnqn=self.subsysnqn, - transport='tcp', - traddr='10.0.0.1', - trsvcid='8009', - ) - ctrl = nvme.ctrl(self.ctx, fctx) + ctrl = nvme.ctrl(self.ctx, { + 'subsysnqn': self.subsysnqn, + 'transport': 'tcp', + 'traddr': '10.0.0.1', + 'trsvcid': '8009', + }) self.assertEqual(ctrl.trsvcid, '8009') def test_connected_returns_false_before_connect(self): @@ -183,12 +175,10 @@ def test_str_contains_transport(self): self.assertIn('loop', s) def test_context_manager(self): - fctx = nvme.fabrics_context(self.ctx) - fctx.set_connection( - subsysnqn=self.subsysnqn, - transport='loop' - ) - with nvme.ctrl(self.ctx, fctx) as c: + with nvme.ctrl(self.ctx, { + 'subsysnqn': self.subsysnqn, + 'transport': 'loop', + }) as c: self.assertIsNotNone(c) def test_namespaces_iterator_returns_list(self): @@ -234,12 +224,10 @@ class TestCtrlErrorHandling(unittest.TestCase): def setUp(self): self.ctx = nvme.global_ctx() - fctx = nvme.fabrics_context(self.ctx) - fctx.set_connection( - subsysnqn=nvme.NVME_DISC_SUBSYS_NAME, - transport='loop', - ) - self.ctrl = nvme.ctrl(self.ctx, fctx) + self.ctrl = nvme.ctrl(self.ctx, { + 'subsysnqn': nvme.NVME_DISC_SUBSYS_NAME, + 'transport': 'loop', + }) def tearDown(self): self.ctrl = None From 60e41d746a0434e297fa163458af9a0b16fb4958 Mon Sep 17 00:00:00 2001 From: Martin Belanger Date: Mon, 20 Apr 2026 18:47:22 -0400 Subject: [PATCH 3/3] swig: fix Py_NewRef compatibility with Python < 3.10 SWIG >= 4.1 generates calls to Py_NewRef() in its runtime boilerplate. Py_NewRef was introduced in Python 3.10, causing an undefined symbol error on older distributions (e.g. SLES 15.6/15.7 with Python 3.6). Add a static inline shim inside the %begin block, guarded by PY_VERSION_HEX, so it precedes SWIG's own runtime in the generated C file. Python.h is included explicitly at that point to make PyObject available. A meson warning is emitted at configure time when the older Python is detected. Signed-off-by: Martin Belanger Assisted-by: Claude Sonnet 4.6 --- libnvme/libnvme/meson.build | 4 ++++ libnvme/libnvme/nvme.i | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/libnvme/libnvme/meson.build b/libnvme/libnvme/meson.build index de4111881a..d2b2aa8851 100644 --- a/libnvme/libnvme/meson.build +++ b/libnvme/libnvme/meson.build @@ -6,6 +6,10 @@ # Authors: Martin Belanger # if want_python + if not cc.has_header_symbol('Python.h', 'Py_NewRef', dependencies: py3_dep) + warning('Python < 3.10 detected: Py_NewRef compatibility shim will be used') + endif + r = run_command(swig, ['-version'], check: true) # Returns: "\nSWIG Version 4.1.1\n\nCompiled with ..." swig_version = r.stdout().split('\n')[1].split()[2].strip() if swig_version.version_compare('<4.1.0') diff --git a/libnvme/libnvme/nvme.i b/libnvme/libnvme/nvme.i index 88520aea5f..714cdbac14 100644 --- a/libnvme/libnvme/nvme.i +++ b/libnvme/libnvme/nvme.i @@ -10,6 +10,20 @@ clashes with the same macro defined in Python.h. */ #undef fallthrough + +#include + +/* WORKAROUND: Py_NewRef() was introduced in Python 3.10. SWIG >= 4.1 generates + calls to it in its runtime boilerplate, which breaks older + distributions (e.g. SLES 15.6/15.7 with Python 3.6). + */ +#if PY_VERSION_HEX < 0x030a0000 +static inline PyObject *Py_NewRef(PyObject *obj) +{ + Py_INCREF(obj); + return obj; +} +#endif %} %module(docstring="Python bindings for libnvme") nvme