Fix AxiStreamDma stop races and zero-copy lifetime - #1277
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adjusts rogue::hardware::axi::AxiStreamDma teardown semantics for zero-copy so that stop() closes only the per-instance DMA file descriptor while keeping the shared mapped DMA buffers alive until ~AxiStreamDma(), preventing downstream zero-copy frames retained past stop() from referencing unmapped memory.
Changes:
- Move shared descriptor (
desc_) cleanup fromstop()into~AxiStreamDma(). - Add
fdMtx_and use it to serialize deferreddmaRetIndex()calls againstfd_closure. - Remove unused locals in
retBuffer()and updatestop()/API comments to reflect the new lifetime model.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/rogue/hardware/axi/AxiStreamDma.cpp | Moves shared mapping cleanup to destructor; adds mutex-guarded fd close and mutex-guarded zero-copy buffer return. |
| include/rogue/hardware/axi/AxiStreamDma.h | Adds <mutex>, introduces fdMtx_, and updates stop() documentation to reflect new lifetime behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…fetime Add a Linux-only C++ doctest that constructs a real AxiStreamDma against an LD_PRELOAD shim faking the aes-stream-driver, retains a zero-copy frame across stop(), and asserts the shared DMA mapping stays mapped after stop() and is released only at destruction. Moving closeShared() back into stop() unmaps the buffers while the frame is still held and fails the post-stop() assertion, so the test guards against regressing the teardown-lifetime fix. The fake-driver shim is a test-only LD_PRELOAD module built only under ROGUE_BUILD_TESTS and never linked into rogue-core. The test is labeled no-python so it runs in both CI ctest jobs, and is skipped on non-Linux where LD_PRELOAD interposition is unavailable.
…clab/rogue into axi-stream-dma-stop-lifetime # Conflicts: # tests/cpp/hardware/CMakeLists.txt
doctest cannot stringify a shared_ptr<Frame>, so a bare REQUIRE(frame) fails to compile. Use static_cast<bool>(frame), matching the workaround already used in the sibling zerocopy lifetime test. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
I have a few concerns with this. I guess which is the error this is really solving. Since the fd_ value is only written twice and open and close and all other blocks read, I would suspect we would be fine without a lock. Were there actual cases where we were seeing segfaults on close because accept_frame was called? Also, I can understand the issue with the allocated buffer returns; that seems like a more real problem. Anyway, we should fully understand what is being solved. My main problem is the solution creates a huge amount of object creation and destruction to implement the lock. I would have preferred a more direct, but maybe less "layered and python" like approach that limited the memory allocations at high rate. The target high rate is also why I am apprehensive about even adding locks so I want to make sure this is really necessary. If we do have to add locks around the fd access, we need to use a clean mutex without the fancy class that wraps it. |
…clab/rogue into axi-stream-dma-stop-lifetime
|
I have reworked the change locally to focus on two cases:
The The public documentation now requires callers to quiesce stream operations and driver controls/accessors before calling The regression coverage has also been narrowed to the two behaviors above: retained mapping lifetime and |
Description
Fix two concrete
AxiStreamDmashutdown hazards:~AxiStreamDma(), so Rogue frames retained afterstop()do not point at unmapped memory.dmaRetIndex()buffer returns against closure of the per-instancefd_, preventingstop()from closing or allowing reuse of the descriptor while a return is in flight.This intentionally does not make every public driver operation safe to run concurrently with
stop(). Callers must quiesce stream operations and driver controls/accessors before callingstop(). Deferred returns from zero-copy buffers already issued by the interface are the one operation explicitly allowed to overlap shutdown.Details
stop()to~AxiStreamDma(). Each RogueBufferretains its allocatingPool, so outstanding DMA-backed buffers keep theAxiStreamDmaobject—and therefore the shared mapping—alive until those buffers are released.std::mutexused only byretBuffer()and the descriptor-close block instop().fd_validity check anddmaRetIndex(), whilestop()holds it acrossclose(fd_)and settingfd_ = -1.stop()idempotent and join the RX thread based onjoinable().LD_PRELOADfake aes-stream-driver shim:stop()and verify the shared mapping remains present until destruction;dmaRetIndex()and verifystop()waits rather than closing the fd during the driver call.Performance consideration
One concern remains partially open: every non-stale zero-copy return now performs an additional mutex lock/unlock before the existing
decCounter()lock. That is far better than locking every allocation, transmit, getter, and control operation, but the PR contains no targeted DMA-return throughput measurement. The general Performance Tests passed, but they do not exercise this fake-driver return path. If high-rate impact is the deciding issue for @slacrherbst, a hardware measurement or focused benchmark would close it properly.Validation
retBuffer()/stop()exclusion allows descriptor close to overlap the blocked driver return and causes the lifecycle regression to fail.git diff --checkpasses.JIRA
N/A
Related
Related to #1276 teardown review.