Skip to content

fix(rocev2.Server): free RX slab in destructor to fix zero-copy teardown segfault - #1276

Open
ruck314 wants to merge 3 commits into
pre-releasefrom
Server-stop
Open

fix(rocev2.Server): free RX slab in destructor to fix zero-copy teardown segfault#1276
ruck314 wants to merge 3 commits into
pre-releasefrom
Server-stop

Conversation

@ruck314

@ruck314 ruck314 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Description

Fix a use-after-free in rogue::protocols::rocev2::Server teardown. stop() freed the registered RX slab while zero-copy Buffers already handed downstream still pointed into it, which surfaced as a Segmentation fault at process/GUI teardown. The slab is now freed in ~Server (its lifetime is the Pool object's, as with ris::Pool::~Pool), while stop() releases only the external ibverbs resources (QP/CQ/MR/comp-channel/wake pipe), matching AxiStreamDma::stop. Also adds the missing rogue::GilRelease to the constructor, completeConnection, retBuffer, and stop() (matching AxiStreamDma/Pool), plus a hardware-gated regression test that reproduces the UAF over an rxe0 loopback.

Details

  • Server::stop()cleanupResources() previously free()d slab_; a frame still queued in a downstream depacketizer / PrbsRx then became a dangling pointer. Every zero-copy Buffer holds a shared_ptr to its Pool (the Server), so ~Server cannot run until all outstanding Buffers are released — the safe place to free the slab. cleanupResources() drops its freeSlab argument; the failed-construction path frees the slab explicitly (the destructor does not run on a partially-constructed object).
  • Missing rogue::GilRelease added to the ibverbs bring-up (constructor, completeConnection), the Buffer::~Buffer re-post (retBuffer), and teardown (stop() — which joins the recv thread that re-acquires the GIL via sendFrame), matching the convention already used in AxiStreamDma and the base Pool.
  • New tests/cpp/protocols/rocev2/test_rocev2_teardown.cpp (hardware-gated on rxe0): loopback RDMA SEND → retain the received frame past stop() → assert the payload survives. It is RED on the old teardown and GREEN with the fix. Full ctest -L cpp (16/16) and the Python rocev2 suite (35/35) pass; validated end-to-end against a 10GbE RoCEv2 firmware target (25/25 PRBS runs, no segfault).

Copilot AI 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.

Pull request overview

This PR fixes a RoCEv2 Server teardown use-after-free affecting zero-copy RX buffers by moving RX slab deallocation to ~Server (so it can’t be freed while downstream still holds Buffers), while keeping stop() responsible for releasing ibverbs/FD resources. It also aligns RoCEv2 bring-up/teardown paths with the project’s Python GIL-release convention and adds a hardware-gated regression test that reproduces the original failure mode on rxe0.

Changes:

  • Prevent zero-copy UAF by freeing the RX slab in rogue::protocols::rocev2::Server::~Server() instead of during stop()/cleanupResources().
  • Add rogue::GilRelease around ibverbs bring-up, buffer return repost, and teardown to avoid blocking/deadlocking Python threads.
  • Add an rxe0 loopback regression test (isolated binary) that retains a frame past stop() and validates payload integrity.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/rogue/protocols/rocev2/Server.cpp Moves slab lifetime to destructor, adjusts cleanup semantics, and adds GilRelease in key paths.
include/rogue/protocols/rocev2/Server.h Updates cleanupResources() contract docs to reflect slab lifetime change.
tests/cpp/protocols/rocev2/test_rocev2_teardown.cpp Adds a hardware-gated regression reproducer for the zero-copy teardown UAF.
tests/cpp/protocols/rocev2/CMakeLists.txt Builds the new teardown regression as an isolated test binary.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/cpp/protocols/rocev2/test_rocev2_teardown.cpp
Comment thread tests/cpp/protocols/rocev2/test_rocev2_teardown.cpp Outdated

Copilot AI 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.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment thread tests/cpp/protocols/rocev2/test_rocev2_teardown.cpp
Comment thread tests/cpp/protocols/rocev2/test_rocev2_teardown.cpp

Copilot AI 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.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment thread tests/cpp/protocols/rocev2/test_rocev2_teardown.cpp

Copilot AI 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.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

@ruck314
ruck314 requested a review from bengineerd July 3, 2026 00:40
@ruck314
ruck314 marked this pull request as ready for review July 3, 2026 00:40
ruck314 added 3 commits July 2, 2026 17:49
…ro-copy UAF

stop() -> cleanupResources() previously free()d the registered RX slab
while zero-copy Buffers already handed downstream still pointed into it,
surfacing as a segfault at process/GUI teardown. Every zero-copy Buffer
holds a shared_ptr to its Pool (the Server), so ~Server cannot run until
all outstanding Buffers are released -- that is the safe place to free
the slab, mirroring ris::Pool::~Pool. stop() now releases only the
external ibverbs resources (QP/CQ/MR/comp-channel/wake pipe), matching
AxiStreamDma::stop(); the failed-construction path frees the slab
explicitly since the destructor does not run on a partial object.

Also add the missing rogue::GilRelease to the ibverbs bring-up
(constructor, completeConnection), the Buffer::~Buffer re-post
(retBuffer), and teardown (stop(), which joins the recv thread that
re-acquires the GIL via sendFrame), matching AxiStreamDma and Pool.
New tests/cpp/protocols/rocev2/test_rocev2_teardown.cpp (gated on an
rxe0 SoftRoCE device): loopback RDMA SEND, retain the received frame
past Server::stop(), and assert the payload survives. RED on the old
teardown (slab freed in stop()), GREEN with the destructor-frees-slab
fix. The LoopbackClient uses RAII cleanup, scans the full GID table to
match the complete IPv4-mapped prefix, and bound-checks the GID index
before the uint8_t truncation.
Match the C++ API page to the teardown fix: stop() releases the
QP/CQ/MR but no longer frees the RX slab, and ~Server() frees it last
after the final zero-copy Buffer releases the Server.

@bengineerd bengineerd 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.

Review findings from local inspection of PR head 4fa2644.

CHECK_EQ(std::memcmp(after.data(), client.sbuf.data(), kPayloadLen), 0);

// Release the held frame before the client MR/QP it (indirectly) races.
frame.reset();

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.

This does not actually release the last held frame. server owns slave via addSlave(), HoldingSlave::frames_ owns the FramePtr, and that frame's Buffer owns a shared_ptr back to the Server. frame.reset() only drops the local copy, so the test exits with a Server -> slave -> frame -> buffer -> Server cycle and ~Server() never frees the slab. Please add a HoldingSlave::clear() (or similar) and call it here before leaving the test.

// retBuffer runs from Buffer::~Buffer(), which can fire on a Python thread
// holding the GIL; release it around the ibv_post_recv re-post and decCounter
// (which locks). Matches ris::Pool::retBuffer() and AxiStreamDma::retBuffer().
rogue::GilRelease noGil;

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 re-post check just below this is still racing teardown. A downstream frame can be released on another thread while stop() is past threadEn_.store(false) and about to destroy qp_/deregister mr_ in cleanupResources(). Since retBuffer() reads threadEn_ and qp_ without synchronizing with cleanupResources(), it can enter postRecvWr() with a QP/MR being destroyed. The re-post path and resource cleanup need shared exclusion (for example, a mutex around the threadEn_/qp_/mr_ check+post and cleanupResources()), or teardown needs another way to wait out active retBuffer() calls.

@bengineerd bengineerd 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.

Additional review note on the AxiStreamDma teardown comparison. The AxiStreamDma follow-up is now split into #1277.

thread_ = nullptr;
}
// Release the external ibverbs resources now — QP/CQ/MR(dereg)/comp-channel
// and the wake pipe — mirroring AxiStreamDma::stop(), which releases its

@bengineerd bengineerd Jul 6, 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.

Update: I split the AxiStreamDma side of this into #1277.

I would not use AxiStreamDma::stop() as the lifecycle precedent here without caveating it. Looking at AxiStreamDma, it appears to have the same latent issues: RX zero-copy frames are created over desc_->rawBuff[...], stop() can call closeShared(desc_) and dmaUnMapDma() that mapping while downstream frames may still hold those buffers, and retBuffer() checks fd_ >= 0 before dmaRetIndex(fd_, ...) without synchronizing against stop() closing fd_. So matching the old AxiStreamDma behavior does not prove this teardown contract is safe; the AxiStreamDma fix is now tracked separately in #1277, and this PR should avoid presenting the old AxiStreamDma behavior as a safe model.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants