Skip to content

NIXL: Zero-copy nixlDescList - #1324

Closed
iyastreb wants to merge 6 commits into
ai-dynamo:mainfrom
iyastreb:zero-copy
Closed

iyastreb wants to merge 6 commits into
ai-dynamo:mainfrom
iyastreb:zero-copy

Conversation

@iyastreb

@iyastreb iyastreb commented Feb 13, 2026 •

Copy link
Copy Markdown
Contributor

What?

Enabled zero-copy for nixlDescList by allowing shallow copy.

Why?

On large desc dimensions copying takes a lot of time. One of the copies is made inside Python binding, where we copy Python array into nixlDescList. This copy takes quite some time while holding the GIL lock. In certain cases we can eliminate this copy by making it shallow copy, when initialize_xfer is called sequentially after get_xfer_descs.
initialize_xfer makes its own copy (nixlMetaList), which can be also eliminated.

The proposed solution is to optionally enable zero-copy mode for nixlDescList. Appropriate Python binding modification will be done in a separate PR.

How?

Added a common view_ that points either to owned descs or to an external data. All the read accesses are done via this view, all the writes modify the view.

Python API is in separate PR: #1325

Perf testing:

before (us)     after (us)
calc: 1368233   1373060
desc: 453230    15355    (30x faster, copy eliminated)
prep: 3183194   1872914  (access is faster: inline non-virtual [])
post: 816547    781286   (access is faster: inline non-virtual [])

Summary by CodeRabbit

  • New Features

    • Added shallow-copy support for descriptor lists with view-based iteration.
    • Introduced explicit copy/move semantics and a new factory method for creating shallow copies.
    • Added iterator type aliases for improved API consistency.
  • Bug Fixes

    • Added runtime protection to prevent modifications to shallow-copy descriptor lists.
  • API Changes

    • Updated method signatures for parameter types and iteration interfaces across descriptor list operations.

@github-actions

Copy link
Copy Markdown

👋 Hi iyastreb! Thank you for contributing to ai-dynamo/nixl.

Your PR reviewers will review your contribution then trigger the CI to test your changes.

🚀

@iyastreb

Copy link
Copy Markdown
Contributor Author

/build

@ColinNV ColinNV left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And as discussed I believe it would be cleaner and less error-prone to keep the two cases (owning, non-owning) somehow separate, however that's now possible at this point.

* Can be called only in owner mode (not shallow copy), after vector is modified
*/
inline void
syncView() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline is superfluous, a noexcept could be added.

* @param size Size of the descriptor list
* @return Shallow copy of the descriptor list
*/
static const nixlDescList<T>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const does nothing when returning by value.

/** @var Vector for storing nixlDescs */
std::vector<T> descs;
/** @var Whether the descriptor list is a shallow copy */
bool isShallowCopy_;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether "shallow copy" is the right term, how about something like non-owning view, or read-only view?

Comment thread src/api/cpp/nixl_descriptors.h Outdated
return size_;
}

inline size_t

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inline does nothing here, noexcept can be added.

inline bool
isEmpty() const {
return (descs.size() == 0);
return (size_ == 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brackets are superfluous (as is the inline while noexcept is missing).

if (size <= 1) return (size == 1);
for (int i = 0; i < size - 1; ++i) {
if (vec[i + 1] < vec[i]) return false;
size_t count = size();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const

if (vec[i + 1] < vec[i]) return false;
size_t count = size();
if (count <= 1) {
return (count == 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brackets are superfluous, and why is an empty list considered not sorted?

if (count <= 1) {
return (count == 1);
}
for (size_t i = 0; i < count - 1; ++i) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could probably be implemented with std::is_sorted() or std::is_sorted_until().

if (itr == this->descs.end()) return NIXL_ERR_NOT_FOUND;
if (static_cast<const nixlBasicDesc &>(*itr) == query)
return static_cast<int>(itr - this->descs.begin());
auto itr = std::lower_bound(begin(), end(), query);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const

auto itr = std::lower_bound(this->descs.begin(), this->descs.end(), query);
if (itr != this->descs.end() && itr->covers(query))
return static_cast<int>(itr - this->descs.begin());
auto itr = std::lower_bound(begin(), end(), query);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const

void
nixlDescList<T>::remDesc(size_t index) {
checkModifiable();
if ((index >= descs.size()) || (index < 0)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ((index >= descs.size()) || (index < 0)) {
if (index >= descs.size()) {

index < 0 is always false for size_t

/** @var Whether the descriptor list is a shallow copy */
bool isShallowCopy_;
/** @var Common view of the descriptor list, both for shallow copy and own data */
const T *view_;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we also use std::variant to hold either the "view" type (data + size) or a std::vector<T>?
It might not be as simple but it would avoid any bugs related to using descs while _isShallowCopy is true

nixlDescList<nixlBasicDesc> trimmed(type);
nixlBasicDesc* p;

for (auto & elm: descs) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Iterating over descs will not work here when it's a shallow copy

std::string(reinterpret_cast<const char *>(begin()), n_desc * sizeof(nixlBasicDesc)));
if (ret) return ret;
} else { // already checked it can be only nixlBlobDesc or nixlSectionDesc
for (auto & elm : descs) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Iterating over descs will not work here when it's a shallow copy

Comment on lines 409 to 410
for (size_t i=0; i<lhs.descs.size(); ++i)
if (lhs.descs[i] != rhs.descs[i])

@guy-ealey-morag guy-ealey-morag Feb 18, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accesing descs directly will not work here when it's a shallow copy.

@coderabbitai

coderabbitai Bot commented Mar 11, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

The changes introduce shallow-copy semantics to nixlDescList<T> with new private members (isShallowCopy_, view_, size_) for managing shared descriptor views. Constructor signatures are updated from int to size_t, explicit copy/move operations are added, and iteration/access methods are adapted to support both owning and view-based models with runtime protection.

Changes

Cohort / File(s) Summary
Header: Public API and Ownership Model
src/api/cpp/nixl_descriptors.h
Introduced shallow-copy semantics with private members, new static makeShallowCopy factory, updated constructors (explicit copy/move), iterator type aliases, and adapted public accessors to use view-based access. Constructor parameter changed from int to size_t. New runtime checks prevent modification of shallow copies.
Implementation: Constructors and Core Logic
src/infra/nixl_descriptors.cpp
Implemented new constructors (shallow-copy, copy, move) with initializer lists and syncView() calls. Updated addDesc and remDesc with modifiable checks and view synchronization. Modified serialize, getIndex, and iteration patterns to use begin/end iterators and the view model. Updated nixlSecDescList methods to work with new semantics.
Section Descriptor Header: Signature Alignment
src/infra/mem_section.h
Updated method signatures to align with base class changes: addDesc now passes-by-value, resize parameter changed from const reference to by-value. Minor implementation detail in operator[] delegates to base class.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A shallow copy hops with grace,
Views shared across the memory space,
Constructors dance in move and clone,
Guards protect what's not our own,
Descriptors leap through synced delight! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'NIXL: Zero-copy nixlDescList' clearly and concisely describes the main change—introducing zero-copy/shallow-copy capability for the nixlDescList class.
Description check ✅ Passed The description follows the required template with complete What/Why/How sections. It clearly explains the feature, its motivation (performance optimization), and implementation approach (view-based model).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/infra/nixl_descriptors.cpp (1)

189-243: ⚠️ Potential issue | 🟡 Minor

Deserializer calls clear() before type is initialized.

Line 197 calls clear(), which calls checkModifiable(). However, at this point type hasn't been deserialized yet (happens at line 207). While isShallowCopy_ is initialized to false in the member initializer list (line 191), calling clear() before full initialization seems fragile. Consider moving clear() after basic validation or initializing descs empty by default.

Additionally, view_ and size_ are initialized to nullptr and 0 but syncView() is only called after deserialization succeeds, leaving them inconsistent during parsing.

Suggested improvement
 template<class T>
 nixlDescList<T>::nixlDescList(nixlSerDes *deserializer)
     : isShallowCopy_(false),
       view_(nullptr),
       size_(0) {
     size_t n_desc;
     std::string str;

-    clear();
+    // descs is default-constructed empty, no need to clear
+    // syncView will be called after successful parsing

     str = deserializer->getStr("nixlDList"); // Object type
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/infra/nixl_descriptors.cpp` around lines 189 - 243, The constructor
nixlDescList<T>::nixlDescList currently calls clear() before 'type' is
deserialized which triggers checkModifiable() and can be fragile; move the
clear() call to after the initial validation that reads "nixlDList" and
successfully deserializes 'type' and 'n' (the getBuf("t", &type, ...) and
getBuf("n", &n_desc, ... ) calls) so clear() only runs once the object has a
valid type, or alternatively ensure descs is default-initialized empty and
clear() does not rely on an initialized 'type' (update clear() to be safe when
type is uninitialized); also ensure view_ and size_ remain consistent by calling
syncView() only after deserialization succeeds (leave view_/size_ as nullptr/0
until then) and reference the functions/members: clear(), getStr(), getBuf("t",
&type...), getBuf("n", &n_desc...), descs, isShallowCopy_, view_, size_, and
syncView() when making changes.
src/api/cpp/nixl_descriptors.h (1)

17-18: 🧹 Nitpick | 🔵 Trivial

Header guard doesn't follow full-path convention.

Per repository conventions, header guards should include the full repository-relative path in uppercase with NIXL_ prefix. Consider updating to NIXL_SRC_API_CPP_NIXL_DESCRIPTORS_H.

Suggested fix
-#ifndef _NIXL_DESCRIPTORS_H
-#define _NIXL_DESCRIPTORS_H
+#ifndef NIXL_SRC_API_CPP_NIXL_DESCRIPTORS_H
+#define NIXL_SRC_API_CPP_NIXL_DESCRIPTORS_H

And at the end of the file:

-#endif
+#endif // NIXL_SRC_API_CPP_NIXL_DESCRIPTORS_H

Based on learnings: "Enforce header guards to include the full repository-relative path in uppercase, with slashes replaced by underscores, prefixed by NIXL_, and suffixed with _H."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/cpp/nixl_descriptors.h` around lines 17 - 18, The header guard
currently uses _NIXL_DESCRIPTORS_H; update it to follow repo convention by
replacing both occurrences of the guard with NIXL_SRC_API_CPP_NIXL_DESCRIPTORS_H
(and update the trailing `#endif` comment if present) so the guard includes the
full repository-relative path in uppercase with underscores and the NIXL_ prefix
and _H suffix.
♻️ Duplicate comments (3)
src/infra/nixl_descriptors.cpp (3)

285-295: ⚠️ Potential issue | 🟠 Major

trim() iterates over descs directly — broken for shallow copies.

When isShallowCopy_ is true, descs is empty and this loop produces an empty result instead of trimming the actual view data.

Suggested fix
 template <class T>
 nixlDescList<nixlBasicDesc> nixlDescList<T>::trim() const {

     if constexpr (std::is_same<nixlBasicDesc, T>::value) {
         return *this;
     } else {
         nixlDescList<nixlBasicDesc> trimmed(type);
-        nixlBasicDesc* p;
-
-        for (auto & elm: descs) {
-            p = (nixlBasicDesc*) (&elm);
-            trimmed.addDesc(*p);
+        for (const auto &elm : *this) {
+            trimmed.addDesc(static_cast<const nixlBasicDesc &>(elm));
         }

         // No failure scenario
         return trimmed;
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/infra/nixl_descriptors.cpp` around lines 285 - 295, The loop in trim()
iterates over descs, which is empty for shallow copies (isShallowCopy_ == true),
so change the iteration to use the actual view container when shallow: check
isShallowCopy_ and iterate over the view backing (e.g., the container that holds
the view elements / descriptors) instead of descs, then cast each element to
nixlBasicDesc* and call trimmed.addDesc(*p) as before; implement this by
branching on isShallowCopy_ (or by selecting the correct container via a helper
like getViewDescs()) so trimmed contains the real descriptors for shallow
copies.

387-395: ⚠️ Potential issue | 🟠 Major

operator== accesses descs directly — broken for shallow copies.

Lines 391-393 iterate over lhs.descs and rhs.descs, which are empty for shallow copies, causing false equality results.

Suggested fix
 template <class T>
 bool operator==(const nixlDescList<T> &lhs, const nixlDescList<T> &rhs) {
-    if ((lhs.getType() != rhs.getType()) || (lhs.descCount() != rhs.descCount())) return false;
+    if ((lhs.getType() != rhs.getType()) || (lhs.size() != rhs.size())) return false;

-    for (size_t i=0; i<lhs.descs.size(); ++i)
-        if (lhs.descs[i] != rhs.descs[i])
+    for (size_t i = 0; i < lhs.size(); ++i)
+        if (lhs[i] != rhs[i])
             return false;
     return true;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/infra/nixl_descriptors.cpp` around lines 387 - 395, The operator==
implementation in nixlDescList<T> directly accesses the private member descs (in
operator==(const nixlDescList<T>&,...)) which is empty for shallow copies;
instead iterate using descCount() and compare elements via the class's public
accessor (e.g., getDesc(i) or descAt(i)) for both lhs and rhs to avoid touching
the internal vector; modify the loop in operator== to run i from 0 to
lhs.descCount() and compare lhs.getDesc(i) with rhs.getDesc(i) (using the actual
accessor name defined on nixlDescList<T>).

341-346: ⚠️ Potential issue | 🟠 Major

serialize() iterates over descs directly — broken for shallow copies.

The else-branch (lines 343-346) iterates for (auto & elm : descs) which is empty for shallow copies, producing incorrect serialization output.

Suggested fix
     } else { // already checked it can be only nixlBlobDesc or nixlSectionDesc
-        for (auto & elm : descs) {
+        for (const auto &elm : *this) {
             ret = serializer->addStr("", elm.serialize());
             if (ret) return ret;
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/infra/nixl_descriptors.cpp` around lines 341 - 346, serialize() is
iterating over the local variable descs which can be an empty shallow copy,
causing missing output; change the loop to iterate the actual container (e.g.,
use this->descs or accept/use a const reference to the original container) so
serializer->addStr("", elm.serialize()) runs against the real elements (replace
for (auto & elm : descs) with for (auto & elm : this->descs) or adjust the
function signature to take const std::vector<...>& descs and iterate that).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/cpp/nixl_descriptors.h`:
- Around line 384-387: The descCount() method currently returns int while
reading size_ (a size_t), risking silent narrowing; change descCount() to return
size_t to match size(), or if int is required for API compatibility, add an
explicit bounds check in descCount() that caps values > INT_MAX (using size_ and
INT_MAX) and handles overflow (e.g., throw or return INT_MAX) to avoid undefined
behavior—update the signature and any callers of descCount() accordingly (refer
to descCount() and size_).
- Around line 480-481: The remDesc(size_t) declaration causes negative ints
(e.g. from getIndex()) to wrap to huge unsigned values and bypass the (index <
0) check; fix by reverting remDesc to accept a signed int (e.g. remDesc(int
index)) or, if you must keep size_t, add explicit validation at the top of the
implementation: checkModifiable(); verify index is within bounds and not
negative by using a signed comparison (cast descs.size() to int or check the
source int before conversion) and throw std::out_of_range on failure before
calling descs.erase or syncView; update call sites (e.g. where getIndex() is
used in src/infra/nixl_memory_section.cpp and the Rust C API wrappers) if you
change the signature.

---

Outside diff comments:
In `@src/api/cpp/nixl_descriptors.h`:
- Around line 17-18: The header guard currently uses _NIXL_DESCRIPTORS_H; update
it to follow repo convention by replacing both occurrences of the guard with
NIXL_SRC_API_CPP_NIXL_DESCRIPTORS_H (and update the trailing `#endif` comment if
present) so the guard includes the full repository-relative path in uppercase
with underscores and the NIXL_ prefix and _H suffix.

In `@src/infra/nixl_descriptors.cpp`:
- Around line 189-243: The constructor nixlDescList<T>::nixlDescList currently
calls clear() before 'type' is deserialized which triggers checkModifiable() and
can be fragile; move the clear() call to after the initial validation that reads
"nixlDList" and successfully deserializes 'type' and 'n' (the getBuf("t", &type,
...) and getBuf("n", &n_desc, ... ) calls) so clear() only runs once the object
has a valid type, or alternatively ensure descs is default-initialized empty and
clear() does not rely on an initialized 'type' (update clear() to be safe when
type is uninitialized); also ensure view_ and size_ remain consistent by calling
syncView() only after deserialization succeeds (leave view_/size_ as nullptr/0
until then) and reference the functions/members: clear(), getStr(), getBuf("t",
&type...), getBuf("n", &n_desc...), descs, isShallowCopy_, view_, size_, and
syncView() when making changes.

---

Duplicate comments:
In `@src/infra/nixl_descriptors.cpp`:
- Around line 285-295: The loop in trim() iterates over descs, which is empty
for shallow copies (isShallowCopy_ == true), so change the iteration to use the
actual view container when shallow: check isShallowCopy_ and iterate over the
view backing (e.g., the container that holds the view elements / descriptors)
instead of descs, then cast each element to nixlBasicDesc* and call
trimmed.addDesc(*p) as before; implement this by branching on isShallowCopy_ (or
by selecting the correct container via a helper like getViewDescs()) so trimmed
contains the real descriptors for shallow copies.
- Around line 387-395: The operator== implementation in nixlDescList<T> directly
accesses the private member descs (in operator==(const nixlDescList<T>&,...))
which is empty for shallow copies; instead iterate using descCount() and compare
elements via the class's public accessor (e.g., getDesc(i) or descAt(i)) for
both lhs and rhs to avoid touching the internal vector; modify the loop in
operator== to run i from 0 to lhs.descCount() and compare lhs.getDesc(i) with
rhs.getDesc(i) (using the actual accessor name defined on nixlDescList<T>).
- Around line 341-346: serialize() is iterating over the local variable descs
which can be an empty shallow copy, causing missing output; change the loop to
iterate the actual container (e.g., use this->descs or accept/use a const
reference to the original container) so serializer->addStr("", elm.serialize())
runs against the real elements (replace for (auto & elm : descs) with for (auto
& elm : this->descs) or adjust the function signature to take const
std::vector<...>& descs and iterate that).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ef38289c-51b0-470f-ba7d-c6636e464c30

📥 Commits

Reviewing files that changed from the base of the PR and between 1870127 and 391f325.

📒 Files selected for processing (3)
  • src/api/cpp/nixl_descriptors.h
  • src/infra/mem_section.h
  • src/infra/nixl_descriptors.cpp

Comment on lines +384 to +387
int
descCount() const {
return descs.size();
return size_;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential narrowing conversion from size_t to int.

descCount() returns int but size_ is size_t. For lists exceeding INT_MAX elements, this silently overflows. Consider returning size_t or adding a narrowing check. The size() method correctly returns size_t.

Suggested fix
-    int
+    size_t
     descCount() const {
         return size_;
     }

Or, if int return is required for API compatibility:

     int
     descCount() const {
+        assert(size_ <= static_cast<size_t>(std::numeric_limits<int>::max()));
         return size_;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
int
descCount() const {
return descs.size();
return size_;
}
size_t
descCount() const {
return size_;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/cpp/nixl_descriptors.h` around lines 384 - 387, The descCount()
method currently returns int while reading size_ (a size_t), risking silent
narrowing; change descCount() to return size_t to match size(), or if int is
required for API compatibility, add an explicit bounds check in descCount() that
caps values > INT_MAX (using size_ and INT_MAX) and handles overflow (e.g.,
throw or return INT_MAX) to avoid undefined behavior—update the signature and
any callers of descCount() accordingly (refer to descCount() and size_).

Comment on lines 480 to +481
void
remDesc(const int &index);
remDesc(size_t index);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify callers of remDesc that may pass negative values

echo "=== Checking remDesc callers with int index ==="
rg -n -B2 -A2 'remDesc\(' --type cpp

echo ""
echo "=== Checking getIndex return type and usage ==="
rg -n 'int.*getIndex|getIndex.*int' --type cpp

Repository: ai-dynamo/nixl

Length of output: 4346


🏁 Script executed:

# Check remDesc implementation
sed -n '259,270p' src/infra/nixl_descriptors.cpp

echo ""
echo "=== Check Rust wrapper signatures ==="
sed -n '1080,1100p' src/bindings/rust/wrapper.cpp
sed -n '1275,1295p' src/bindings/rust/wrapper.cpp

echo ""
echo "=== Check NIXL_ERR_NOT_FOUND definition ==="
rg -n 'NIXL_ERR_NOT_FOUND|define.*NOT_FOUND' --type cpp --type h | head -20

echo ""
echo "=== Check getIndex implementation ==="
sed -n '298,320p' src/infra/nixl_descriptors.cpp

Repository: ai-dynamo/nixl

Length of output: 4056


🏁 Script executed:

rg -n 'define NIXL_ERR_NOT_FOUND|NIXL_ERR_NOT_FOUND\s*=' --type h

Repository: ai-dynamo/nixl

Length of output: 114


Critical: remDesc(size_t) silently converts negative indices to large values.

The signature change from int to size_t introduces a dangerous implicit conversion. Callers pass int values directly from getIndex() (which returns -4 for not-found), including in src/infra/nixl_memory_section.cpp and C API wrappers in src/bindings/rust/wrapper.cpp.

When a negative int is passed, it converts to a very large size_t value, and the bounds check (index < 0) in the implementation is always false for an unsigned type, allowing out-of-bounds access.

Suggested fix: Keep int signature with proper validation
     void
-    remDesc(size_t index);
+    remDesc(int index);

Or add explicit validation in the implementation:

void
nixlDescList<T>::remDesc(int index) {
    checkModifiable();
    if ((index >= (int)descs.size()) || (index < 0)) {
        throw std::out_of_range("Index is out of range");
    }
    descs.erase(descs.begin() + index);
    syncView();
}

Note: The current check (index < 0) when index is size_t is always false and does not prevent the conversion of negative values to large unsigned numbers.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void
remDesc(const int &index);
remDesc(size_t index);
void
remDesc(int index);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/cpp/nixl_descriptors.h` around lines 480 - 481, The remDesc(size_t)
declaration causes negative ints (e.g. from getIndex()) to wrap to huge unsigned
values and bypass the (index < 0) check; fix by reverting remDesc to accept a
signed int (e.g. remDesc(int index)) or, if you must keep size_t, add explicit
validation at the top of the implementation: checkModifiable(); verify index is
within bounds and not negative by using a signed comparison (cast descs.size()
to int or check the source int before conversion) and throw std::out_of_range on
failure before calling descs.erase or syncView; update call sites (e.g. where
getIndex() is used in src/infra/nixl_memory_section.cpp and the Rust C API
wrappers) if you change the signature.

@iyastreb

iyastreb commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

It's not relevant for 2 step API, which is now being used

This branch was previously deployed

2 inactive (1 outdated) deployments
SWX_AWS — 391f3257 Deployed Mar 11, 2026 by copy-pr-bot[bot] via Run AWS Tests - Rust Tests #6142
GITLAB — fd529da9 Deployed Feb 13, 2026 by copy-pr-bot[bot] via Trigger CI Pipeline #6900
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants