From 7ffdb647af800f2862afdd76a178dbd06e0120dc Mon Sep 17 00:00:00 2001 From: gly11 Date: Wed, 27 May 2026 12:36:28 +0800 Subject: [PATCH 01/22] feat(sbp2): add session registry and command flow --- ASFWDriver/Controller/ControllerCore.hpp | 6 + .../Controller/ControllerCoreDiscovery.cpp | 23 +- .../Controller/ControllerCoreFacades.cpp | 11 +- ASFWDriver/Logging/Logging.cpp | 1 + ASFWDriver/Logging/Logging.hpp | 1 + .../Protocols/SBP2/AddressSpaceManager.hpp | 52 +- ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp | 106 +- ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp | 34 +- .../Protocols/SBP2/SBP2DelayedDispatch.hpp | 16 + .../Protocols/SBP2/SBP2LoginSession.cpp | 1719 +++++++++++++++++ .../Protocols/SBP2/SBP2LoginSession.hpp | 486 +++++ .../Protocols/SBP2/SBP2ManagementORB.cpp | 309 ++- .../Protocols/SBP2/SBP2ManagementORB.hpp | 48 +- ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp | 7 +- .../Protocols/SBP2/SBP2SessionRegistry.cpp | 668 +++++++ .../Protocols/SBP2/SBP2SessionRegistry.hpp | 143 ++ ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp | 39 +- ASFWDriver/Service/DriverContext.cpp | 284 ++- .../UserClient/Core/ASFWDriverUserClient.cpp | 40 + .../UserClient/Core/ASFWDriverUserClient.iig | 10 + .../Core/UserClientRuntimeState.hpp | 10 +- .../UserClient/Handlers/SBP2Handler.hpp | 277 ++- .../WireFormats/SBP2CommandWireFormats.hpp | 27 + tests/AddressSpaceManagerTests.cpp | 35 + tests/CMakeLists.txt | 136 +- tests/SBP2LoginSessionTests.cpp | 571 ++++++ tests/SBP2ORBTests.cpp | 216 ++- tests/SBP2SessionRegistryTests.cpp | 558 ++++++ 28 files changed, 5440 insertions(+), 393 deletions(-) create mode 100644 ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp create mode 100644 ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp create mode 100644 ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp create mode 100644 ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp create mode 100644 ASFWDriver/UserClient/WireFormats/SBP2CommandWireFormats.hpp create mode 100644 tests/SBP2LoginSessionTests.cpp create mode 100644 tests/SBP2SessionRegistryTests.cpp diff --git a/ASFWDriver/Controller/ControllerCore.hpp b/ASFWDriver/Controller/ControllerCore.hpp index b62d6ab3..194abffd 100644 --- a/ASFWDriver/Controller/ControllerCore.hpp +++ b/ASFWDriver/Controller/ControllerCore.hpp @@ -57,6 +57,7 @@ class FCPResponseRouter; namespace ASFW::Protocols::SBP2 { class AddressSpaceManager; +class SBP2SessionRegistry; } namespace ASFW::IRM { @@ -96,6 +97,7 @@ class ControllerCore { std::shared_ptr avcDiscovery; std::shared_ptr fcpResponseRouter; std::shared_ptr sbp2AddressSpaceManager; + std::shared_ptr sbp2SessionRegistry; std::shared_ptr irmClient; @@ -137,9 +139,13 @@ class ControllerCore { Protocols::AVC::IAVCDiscovery* GetAVCDiscovery() const; void SetAVCDiscovery(std::shared_ptr avcDiscovery); void SetFCPResponseRouter(std::shared_ptr fcpResponseRouter); + Protocols::SBP2::AddressSpaceManager* GetSbp2AddressSpaceManager() const; + Protocols::SBP2::SBP2SessionRegistry* GetSBP2SessionRegistry() const; void SetSbp2AddressSpaceManager( std::shared_ptr sbp2AddressSpaceManager); + void SetSBP2SessionRegistry( + std::shared_ptr sbp2SessionRegistry); IRM::IRMClient* GetIRMClient() const; void SetIRMClient(std::shared_ptr client); diff --git a/ASFWDriver/Controller/ControllerCoreDiscovery.cpp b/ASFWDriver/Controller/ControllerCoreDiscovery.cpp index 956db04c..4e7947fb 100644 --- a/ASFWDriver/Controller/ControllerCoreDiscovery.cpp +++ b/ASFWDriver/Controller/ControllerCoreDiscovery.cpp @@ -30,6 +30,7 @@ #include "../Protocols/AVC/AVCDiscovery.hpp" #include "../Protocols/AVC/CMP/CMPClient.hpp" #include "../Protocols/Audio/DeviceProtocolFactory.hpp" +#include "../Protocols/SBP2/SBP2SessionRegistry.hpp" #include "../Scheduling/Scheduler.hpp" #include "../Version/DriverVersion.hpp" #include "ControllerStateMachine.hpp" @@ -116,6 +117,18 @@ void ControllerCore::OnTopologyReady(const TopologySnapshot& snap) { // CMP (PCR) operations target a *specific device's* plug registers, not the IRM node. // Device-scoped CMP wiring is done at stream start time (IsochService). } + + if (deps_.sbp2SessionRegistry) { + deps_.sbp2SessionRegistry->OnBusReset(static_cast(snap.generation)); + } + + // NOTE: CSR STATE_SET CMSTR write removed. Apple IOFireWireFamily does NOT write + // CSR STATE_SET via async transactions — it uses the OHCI LinkControl register + // directly (kCycleMaster bit), which ASFWDriver already sets in kDefaultLinkControl + // during controller initialization. Async loopback to the local node does not work + // in ASFWDriver (always returns timeout), so the previous implementation was a no-op. + // OHCI hardware generates cycle-start packets automatically when the node is root + // and kCycleMaster is set in LinkControl. } // NOLINTNEXTLINE(readability-function-cognitive-complexity) @@ -218,7 +231,7 @@ void ControllerCore::OnDiscoveryScanComplete(Discovery::Generation gen, } else if (deps_.deviceManager && zeroRomScanInconclusive) { ASFW_LOG(Discovery, "ROM scan for gen=%u produced 0 ROMs but topology still has remote " - "link-active nodes; keeping existing devices until a conclusive scan", + "link-active nodes — keeping existing devices until a conclusive scan", gen.value); } @@ -226,6 +239,14 @@ void ControllerCore::OnDiscoveryScanComplete(Discovery::Generation gen, ASFW_LOG(Discovery, "Discovery complete: %zu devices processed in gen=%u", roms.size(), gen.value); ASFW_LOG(Discovery, "═══════════════════════════════════════════════════════"); + + if (deps_.sbp2SessionRegistry && !zeroRomScanInconclusive) { + deps_.sbp2SessionRegistry->RefreshTargets(gen); + } else if (deps_.sbp2SessionRegistry) { + ASFW_LOG(Discovery, + "Skipping SBP-2 target refresh for inconclusive zero-ROM scan gen=%u", + gen.value); + } } } // namespace ASFW::Driver diff --git a/ASFWDriver/Controller/ControllerCoreFacades.cpp b/ASFWDriver/Controller/ControllerCoreFacades.cpp index 894c095f..48f2f525 100644 --- a/ASFWDriver/Controller/ControllerCoreFacades.cpp +++ b/ASFWDriver/Controller/ControllerCoreFacades.cpp @@ -104,16 +104,25 @@ void ControllerCore::SetFCPResponseRouter( deps_.fcpResponseRouter = std::move(fcpResponseRouter); } +IRM::IRMClient* ControllerCore::GetIRMClient() const { return deps_.irmClient.get(); } + Protocols::SBP2::AddressSpaceManager* ControllerCore::GetSbp2AddressSpaceManager() const { return deps_.sbp2AddressSpaceManager.get(); } +Protocols::SBP2::SBP2SessionRegistry* ControllerCore::GetSBP2SessionRegistry() const { + return deps_.sbp2SessionRegistry.get(); +} + void ControllerCore::SetSbp2AddressSpaceManager( std::shared_ptr sbp2AddressSpaceManager) { deps_.sbp2AddressSpaceManager = std::move(sbp2AddressSpaceManager); } -IRM::IRMClient* ControllerCore::GetIRMClient() const { return deps_.irmClient.get(); } +void ControllerCore::SetSBP2SessionRegistry( + std::shared_ptr sbp2SessionRegistry) { + deps_.sbp2SessionRegistry = std::move(sbp2SessionRegistry); +} void ControllerCore::SetIRMClient(std::shared_ptr client) { deps_.irmClient = std::move(client); diff --git a/ASFWDriver/Logging/Logging.cpp b/ASFWDriver/Logging/Logging.cpp index 5932f8c6..1339886d 100644 --- a/ASFWDriver/Logging/Logging.cpp +++ b/ASFWDriver/Logging/Logging.cpp @@ -31,5 +31,6 @@ os_log_t AVC() { static os_log_t log = MakeCategory("avc"); return os_log_t Isoch() { static os_log_t log = MakeCategory("isoch"); return log; } os_log_t Audio() { static os_log_t log = MakeCategory("audio"); return log; } os_log_t DICE() { static os_log_t log = MakeCategory("dice"); return log; } +os_log_t SBP2() { static os_log_t log = MakeCategory("sbp2"); return log; } } // namespace ASFW::Driver::Logging diff --git a/ASFWDriver/Logging/Logging.hpp b/ASFWDriver/Logging/Logging.hpp index 3bd5585e..9a3c998b 100644 --- a/ASFWDriver/Logging/Logging.hpp +++ b/ASFWDriver/Logging/Logging.hpp @@ -98,6 +98,7 @@ os_log_t AVC(); os_log_t Isoch(); os_log_t Audio(); os_log_t DICE(); +os_log_t SBP2(); } // namespace ASFW::Driver::Logging // ----- time helpers (header-only, safe in DriverKit) ----- diff --git a/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp b/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp index 90801cbd..6410683b 100644 --- a/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp +++ b/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp @@ -2,10 +2,10 @@ #include #include -#include #include #include #include +#include #include #include #include @@ -272,9 +272,10 @@ class AddressSpaceManager { offset = static_cast(address - range->meta.address); ASFW_ADDRSPACE_LOG( - "AddressSpaceManager[%p] remote write addr=0x%012llx len=%zu src=%p " + "AddressSpaceManager[%p] remote write label=%s addr=0x%012llx len=%zu src=%p " "handle=0x%llx rangeAddr=0x%012llx off=%u buf=%p mapped=%p backing=%u", this, + range->debugLabel, static_cast(address), payload.size(), payload.data(), @@ -328,6 +329,18 @@ class AddressSpaceManager { outSlice->payloadDeviceAddress = payloadAddress; outSlice->payloadLength = length; + ASFW_ADDRSPACE_LOG( + "AddressSpaceManager[%p] remote read-block label=%s addr=0x%012llx len=%u " + "handle=0x%llx rangeAddr=0x%012llx off=%llu dma=0x%08x", + this, + range->debugLabel, + static_cast(address), + length, + static_cast(range->meta.handle), + static_cast(range->meta.address), + static_cast(offset), + static_cast(payloadAddress)); + IOLockUnlock(lock_); return Async::ResponseCode::Complete; } @@ -349,6 +362,17 @@ class AddressSpaceManager { range->buffer.data() + static_cast(offset), sizeof(uint32_t)); + ASFW_ADDRSPACE_LOG( + "AddressSpaceManager[%p] remote read-quadlet label=%s addr=0x%012llx " + "handle=0x%llx rangeAddr=0x%012llx off=%u value=0x%08x", + this, + range->debugLabel, + static_cast(address), + static_cast(range->meta.handle), + static_cast(range->meta.address), + offset, + *outValue); + IOLockUnlock(lock_); return Async::ResponseCode::Complete; } @@ -392,6 +416,19 @@ class AddressSpaceManager { IOLockUnlock(lock_); } + void SetDebugLabel(uint64_t handle, const char* label) { + if (!lock_ || handle == 0) { + return; + } + + IOLockLock(lock_); + auto it = ranges_.find(handle); + if (it != ranges_.end()) { + it->second.debugLabel = label != nullptr ? label : "unlabeled"; + } + IOLockUnlock(lock_); + } + void ClearAll() { if (!lock_) { return; @@ -416,6 +453,7 @@ class AddressSpaceManager { void* owner{nullptr}; std::vector buffer; RemoteWriteCallback onRemoteWrite; + const char* debugLabel{"unlabeled"}; OSSharedPtr descriptor{}; OSSharedPtr dmaCommand{}; @@ -485,8 +523,9 @@ class AddressSpaceManager { for (const auto& entry : ranges_) { const auto& range = entry.second; ASFW_ADDRSPACE_LOG( - "AddressSpaceManager[%p] range handle=0x%llx owner=%p addr=0x%012llx len=%u backing=%u dma=0x%08x", + "AddressSpaceManager[%p] range label=%s handle=0x%llx owner=%p addr=0x%012llx len=%u backing=%u dma=0x%08x", this, + range.debugLabel, static_cast(range.meta.handle), range.owner, static_cast(range.meta.address), @@ -549,6 +588,7 @@ class AddressSpaceManager { kern_return_t AllocateBacking(AddressRange& range) { const std::size_t size = static_cast(range.meta.length); + const std::size_t backingSize = static_cast(AlignUp(size, 16)); // IOBufferMemoryDescriptor::Create expects memory-direction options only. // Cache policy is set at CreateMapping time, not in allocation options. @@ -556,7 +596,7 @@ class AddressSpaceManager { std::optional dma; if (hardware_) { - dma = hardware_->AllocateDMA(size, options, 16); + dma = hardware_->AllocateDMA(backingSize, options, 16); } if (!dma.has_value()) { @@ -576,7 +616,7 @@ class AddressSpaceManager { kIOMemoryMapCacheModeInhibit, 0, 0, - size, + backingSize, 0, &mapping); if (kr != kIOReturnSuccess || !mapping) { @@ -597,7 +637,7 @@ class AddressSpaceManager { return kIOReturnNoMemory; } - std::memset(mapped, 0, size); + std::memset(mapped, 0, backingSize); OSSynchronizeIO(); range.descriptor = std::move(dma->descriptor); diff --git a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp index 6179b62e..56b9a609 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp @@ -1,12 +1,11 @@ // SBP-2 Normal Command ORB implementation. +// Ported from Apple IOFireWireSBP2ORB. // Ref: SBP-2 §5.1.1 (Normal Command ORB format) #include "SBP2CommandORB.hpp" #include "SBP2DelayedDispatch.hpp" #include "../../Common/FWCommon.hpp" -#include - namespace ASFW::Protocols::SBP2 { // --------------------------------------------------------------------------- @@ -19,12 +18,11 @@ SBP2CommandORB::SBP2CommandORB(AddressSpaceManager& addrMgr, void* owner, , owner_(owner) , maxCommandBlockSize_(maxCommandBlockSize) { - AllocateResources(); + isValid_ = AllocateResources(); } SBP2CommandORB::~SBP2CommandORB() { CancelTimer(); - lifetimeToken_.reset(); DeallocateResources(); } @@ -33,6 +31,10 @@ SBP2CommandORB::~SBP2CommandORB() { // --------------------------------------------------------------------------- bool SBP2CommandORB::AllocateResources() noexcept { + if (orbHandle_ != 0) { + return true; + } + const uint32_t orbSize = Wire::NormalORB::kHeaderSize + maxCommandBlockSize_; orbStorage_.resize(orbSize, 0); @@ -41,11 +43,11 @@ bool SBP2CommandORB::AllocateResources() noexcept { owner_, 0xFFFF, orbSize, &orbHandle_, &orbMeta_); if (kr != kIOReturnSuccess) { - ASFW_LOG(Async, "SBP2CommandORB: failed to allocate ORB address space: 0x%08x", kr); + ASFW_LOG(SBP2, "SBP2CommandORB: failed to allocate ORB address space: 0x%08x", kr); return false; } - ASFW_LOG(Async, "SBP2CommandORB: allocated %u-byte ORB at %04x:%08x", + ASFW_LOG(SBP2, "SBP2CommandORB: allocated %u-byte ORB at %04x:%08x", orbSize, orbMeta_.addressHi, orbMeta_.addressLo); return true; } @@ -64,6 +66,10 @@ void SBP2CommandORB::DeallocateResources() noexcept { // --------------------------------------------------------------------------- void SBP2CommandORB::SetCommandBlock(std::span cdb) noexcept { + if (!IsValid() || orbStorage_.size() < Wire::NormalORB::kHeaderSize) { + return; + } + const uint32_t copyLen = static_cast( std::min(cdb.size(), static_cast(maxCommandBlockSize_))); @@ -83,9 +89,13 @@ void SBP2CommandORB::SetCommandBlock(std::span cdb) noexcept { // Prepare for execution (fills in dynamic fields) // --------------------------------------------------------------------------- -void SBP2CommandORB::PrepareForExecution(uint16_t localNodeID, - FW::FwSpeed speed, - uint16_t maxPayloadLog) noexcept { +kern_return_t SBP2CommandORB::PrepareForExecution(uint16_t localNodeID, + FW::FwSpeed speed, + uint16_t maxPayloadLog) noexcept { + if (!IsValid() || orbStorage_.size() < sizeof(Wire::NormalORB)) { + return kIOReturnNotReady; + } + auto* orb = reinterpret_cast(orbStorage_.data()); const uint16_t busNodeID = Wire::NormalizeBusNodeID(localNodeID); @@ -151,20 +161,25 @@ void SBP2CommandORB::PrepareForExecution(uint16_t localNodeID, orb->dataSize = dataDescriptor_.dataSize; // Flush ORB to address space - WriteORBToAddressSpace(); + return WriteORBToAddressSpace(); } // --------------------------------------------------------------------------- // Write ORB buffer to DMA-backed address space // --------------------------------------------------------------------------- -void SBP2CommandORB::WriteORBToAddressSpace() noexcept { +kern_return_t SBP2CommandORB::WriteORBToAddressSpace() noexcept { + if (!IsValid() || orbHandle_ == 0) { + return kIOReturnNotReady; + } + const auto span = std::span(orbStorage_.data(), orbStorage_.size()); const kern_return_t kr = addrMgr_.WriteLocalData( owner_, orbHandle_, 0, span); if (kr != kIOReturnSuccess) { - ASFW_LOG(Async, "SBP2CommandORB: failed to write ORB to address space: 0x%08x", kr); + ASFW_LOG(SBP2, "SBP2CommandORB: failed to write ORB to address space: 0x%08x", kr); } + return kr; } // --------------------------------------------------------------------------- @@ -179,62 +194,77 @@ Async::FWAddress SBP2CommandORB::GetORBAddress() const noexcept { return Async::FWAddress(parts); } -void SBP2CommandORB::SetNextORBAddress(uint32_t hi, uint32_t lo) noexcept { +kern_return_t SBP2CommandORB::SetNextORBAddress(uint32_t hi, uint32_t lo) noexcept { + if (!IsValid() || orbStorage_.size() < sizeof(Wire::NormalORB)) { + return kIOReturnNotReady; + } + auto* orb = reinterpret_cast(orbStorage_.data()); orb->nextORBAddressHi = hi; orb->nextORBAddressLo = lo; - WriteORBToAddressSpace(); + return WriteORBToAddressSpace(); } -void SBP2CommandORB::SetToDummy() noexcept { +kern_return_t SBP2CommandORB::SetToDummy() noexcept { + if (!IsValid() || orbStorage_.size() < sizeof(Wire::NormalORB)) { + return kIOReturnNotReady; + } + // Set rq_fmt=3 (bits [13:12] = 11) to make device skip this ORB auto* orb = reinterpret_cast(orbStorage_.data()); uint16_t hostOptions = Wire::FromBE16(orb->options); hostOptions = (hostOptions & ~0x3000u) | 0x6000u; orb->options = Wire::ToBE16(hostOptions); - WriteORBToAddressSpace(); + return WriteORBToAddressSpace(); } // --------------------------------------------------------------------------- // Timer management // --------------------------------------------------------------------------- -void SBP2CommandORB::StartTimer(IODispatchQueue* queue) noexcept { +void SBP2CommandORB::StartTimer(IODispatchQueue* completionQueue, + IODispatchQueue* timeoutQueue) noexcept { CancelTimer(); - if (queue == nullptr || timeoutDuration_ == 0) { + if (completionQueue == nullptr || timeoutQueue == nullptr || timeoutDuration_ == 0) { return; } - timerQueue_ = queue; - inProgress_.store(true, std::memory_order_relaxed); + completionQueue_ = completionQueue; + timerQueue_ = timeoutQueue; + auto timerState = timerState_; + timerState->inProgress.store(true, std::memory_order_relaxed); const uint32_t timeout = timeoutDuration_; const uint64_t expectedGeneration = - timerGeneration_.fetch_add(1, std::memory_order_acq_rel) + 1ULL; - const std::weak_ptr weakLifetime = lifetimeToken_; + timerState->generation.fetch_add(1, std::memory_order_acq_rel) + 1ULL; const uint64_t delayNs = static_cast(timeout) * 1'000'000ULL; - DispatchAfterCompat(queue, delayNs, [this, weakLifetime, expectedGeneration, timeout]() { - if (weakLifetime.expired()) { - return; - } - if (timerGeneration_.load(std::memory_order_acquire) != expectedGeneration || - !inProgress_.load(std::memory_order_relaxed) || - !completionCallback_) { - return; - } - - ASFW_LOG(Async, "SBP2CommandORB: ORB timeout after %u ms", timeout); - inProgress_.store(false, std::memory_order_relaxed); - timerGeneration_.fetch_add(1, std::memory_order_acq_rel); - completionCallback_(-1, Wire::SBPStatus::kUnspecifiedError); + DispatchAfterCompat(timeoutQueue, delayNs, [timerState, + expectedGeneration, + timeout, + completionQueue]() { + DispatchAsyncCompat(completionQueue, [timerState, + expectedGeneration, + timeout]() { + if (timerState->generation.load(std::memory_order_acquire) != expectedGeneration || + !timerState->inProgress.load(std::memory_order_relaxed) || + !timerState->completionCallback) { + return; + } + + ASFW_LOG(SBP2, "SBP2CommandORB: ORB timeout after %u ms", timeout); + timerState->inProgress.store(false, std::memory_order_relaxed); + timerState->generation.fetch_add(1, std::memory_order_acq_rel); + timerState->completionCallback(-1, Wire::SBPStatus::kUnspecifiedError); + }); }); } void SBP2CommandORB::CancelTimer() noexcept { - inProgress_.store(false, std::memory_order_relaxed); + timerState_->inProgress.store(false, std::memory_order_relaxed); + completionQueue_ = nullptr; timerQueue_ = nullptr; - timerGeneration_.fetch_add(1, std::memory_order_acq_rel); + timerState_->generation.fetch_add(1, std::memory_order_acq_rel); } } // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp index cd88d810..98e6658a 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp @@ -3,6 +3,7 @@ // SBP-2 Normal Command ORB. // Represents a single SCSI command submitted to the device after login. // +// Ported from Apple IOFireWireSBP2ORB. // Ref: SBP-2 §5.1.1 (Normal Command ORB format) #include "AddressSpaceManager.hpp" @@ -53,31 +54,36 @@ class SBP2CommandORB { void SetFlags(uint32_t flags) noexcept { flags_ = flags; } void SetMaxPayloadSize(uint16_t bytes) noexcept { maxPayloadSize_ = bytes; } void SetTimeout(uint32_t ms) noexcept { timeoutDuration_ = ms; } - void SetCompletionCallback(CompletionCallback cb) noexcept { completionCallback_ = std::move(cb); } + void SetCompletionCallback(CompletionCallback cb) noexcept { + completionCallback_ = std::move(cb); + timerState_->completionCallback = completionCallback_; + } // Bind page table result from SBP2PageTable::Build. void SetDataDescriptor(const SBP2PageTable::Result& ptResult) noexcept { dataDescriptor_ = ptResult; } - // Internal: called by the session layer before submission. - void PrepareForExecution(uint16_t localNodeID, FW::FwSpeed speed, - uint16_t maxPayloadLog) noexcept; + // Internal: called by SBP2LoginSession before submission. + [[nodiscard]] kern_return_t PrepareForExecution(uint16_t localNodeID, + FW::FwSpeed speed, + uint16_t maxPayloadLog) noexcept; // Internal: ORB address for fetch agent / chaining. [[nodiscard]] Async::FWAddress GetORBAddress() const noexcept; // Internal: set the next ORB pointer (big-endian values). - void SetNextORBAddress(uint32_t hi, uint32_t lo) noexcept; + [[nodiscard]] kern_return_t SetNextORBAddress(uint32_t hi, uint32_t lo) noexcept; // Set rq_fmt=3 (NOP dummy) so device skips this ORB if already fetched. - void SetToDummy() noexcept; + [[nodiscard]] kern_return_t SetToDummy() noexcept; // Internal: timer management. - void StartTimer(IODispatchQueue* queue) noexcept; + void StartTimer(IODispatchQueue* completionQueue, IODispatchQueue* timeoutQueue) noexcept; void CancelTimer() noexcept; // State tracking. + [[nodiscard]] bool IsValid() const noexcept { return isValid_; } [[nodiscard]] bool IsAppended() const noexcept { return isAppended_; } void SetAppended(bool state) noexcept { isAppended_ = state; } @@ -88,9 +94,15 @@ class SBP2CommandORB { [[nodiscard]] CompletionCallback& GetCompletionCallback() noexcept { return completionCallback_; } private: + struct TimerState { + std::atomic inProgress{false}; + std::atomic generation{0}; + CompletionCallback completionCallback{}; + }; + bool AllocateResources() noexcept; void DeallocateResources() noexcept; - void WriteORBToAddressSpace() noexcept; + [[nodiscard]] kern_return_t WriteORBToAddressSpace() noexcept; AddressSpaceManager& addrMgr_; void* owner_; @@ -110,14 +122,14 @@ class SBP2CommandORB { SBP2PageTable::Result dataDescriptor_{}; // State. + bool isValid_{false}; bool isAppended_{false}; - std::atomic inProgress_{false}; uint32_t fetchAgentWriteRetries_{20}; // Timer. + IODispatchQueue* completionQueue_{nullptr}; IODispatchQueue* timerQueue_{nullptr}; - std::atomic timerGeneration_{0}; - std::shared_ptr lifetimeToken_{std::make_shared(0)}; + std::shared_ptr timerState_{std::make_shared()}; }; } // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2DelayedDispatch.hpp b/ASFWDriver/Protocols/SBP2/SBP2DelayedDispatch.hpp index 43ebcf44..6ece74e8 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2DelayedDispatch.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2DelayedDispatch.hpp @@ -12,6 +12,22 @@ namespace ASFW::Protocols::SBP2 { +inline void DispatchAsyncCompat(IODispatchQueue* queue, + std::function callback) noexcept { + if (queue == nullptr || !callback) { + return; + } + +#ifdef ASFW_HOST_TEST + queue->DispatchAsync(std::move(callback)); +#else + auto work = std::move(callback); + queue->DispatchAsync(^{ + work(); + }); +#endif +} + inline void DispatchAfterCompat(IODispatchQueue* queue, uint64_t delayNs, std::function callback) noexcept { diff --git a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp new file mode 100644 index 00000000..69e76687 --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp @@ -0,0 +1,1719 @@ +#include "SBP2LoginSession.hpp" +#include "SBP2DelayedDispatch.hpp" +#include "AddressSpaceManager.hpp" + +#include "../../Async/Interfaces/IFireWireBus.hpp" +#include "../../Async/Interfaces/IFireWireBusInfo.hpp" +#include "../../Common/FWCommon.hpp" + +#include + +namespace ASFW::Protocols::SBP2 { + +using namespace ASFW::Protocols::SBP2::Wire; + +// --------------------------------------------------------------------------- +// Construction / Destruction +// --------------------------------------------------------------------------- + +SBP2LoginSession::SBP2LoginSession(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + AddressSpaceManager& addrSpaceMgr) + : bus_(bus) + , busInfo_(busInfo) + , addrSpaceMgr_(addrSpaceMgr) {} + +SBP2LoginSession::~SBP2LoginSession() { + CancelPendingTimer(); + ClearORBTracking(true); + lifetimeToken_.reset(); + ReleaseOwnedTimeoutQueue(); + DeallocateResources(); +} + +void SBP2LoginSession::SetWorkQueue(IODispatchQueue* queue) noexcept { + workQueue_ = queue; + if (timeoutQueue_ == nullptr) { + EnsureTimeoutQueue(); + } +} + +void SBP2LoginSession::SetTimeoutQueue(IODispatchQueue* queue) noexcept { + timeoutQueue_ = queue; + if (queue != nullptr) { + ReleaseOwnedTimeoutQueue(); + } else { + EnsureTimeoutQueue(); + } +} + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- + +void SBP2LoginSession::Configure(const SBP2TargetInfo& info) noexcept { + targetInfo_ = info; + configured_ = true; + + ASFW_LOG(SBP2, + "SBP2LoginSession: configured target node=0x%04x mgmt_offset=%u LUN=%u " + "mgmt_timeout=%ums max_orb=%u max_cmd_block=%u", + info.targetNodeId, + info.managementAgentOffset, + info.lun, + info.managementTimeoutMs, + info.maxORBSize, + info.maxCommandBlockSize); +} + +// --------------------------------------------------------------------------- +// Login +// --------------------------------------------------------------------------- + +bool SBP2LoginSession::Login() noexcept { + if (!configured_) { + ASFW_LOG(SBP2, "SBP2LoginSession::Login: not configured"); + return false; + } + + if (state_ == LoginState::LoggingIn || state_ == LoginState::LoggedIn) { + ASFW_LOG(SBP2, "SBP2LoginSession::Login: state=%s, ignoring", ToString(state_)); + return false; + } + + // Allocate address spaces for ORB/response/status on first login. + if (!AllocateResources()) { + ASFW_LOG(SBP2, "SBP2LoginSession::Login: resource allocation failed"); + SetState(LoginState::Failed); + return false; + } + + SetState(LoginState::LoggingIn); + loginGeneration_ = static_cast(busInfo_.GetGeneration().value); + loginNodeID_ = targetInfo_.targetNodeId; + + BuildLoginORB(); + + ASFW_LOG(SBP2, + "SBP2LoginSession::Login: sending login ORB to node 0x%04x gen=%u LUN=%u", + loginNodeID_, loginGeneration_, targetInfo_.lun); + + // Write the Login ORB address to the management agent. + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + const Async::FWAddress mgmtAddr{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = 0xFFFF, + .addressLo = ManagementAgentAddressLo(targetInfo_.managementAgentOffset), + .nodeID = loginNodeID_ + } + }; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + loginWriteHandle_ = bus_.WriteBlock( + gen, node, mgmtAddr, + std::span{loginORBAddressBE_.data(), loginORBAddressBE_.size()}, + speed, + [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + OnLoginWriteComplete(requestGeneration, status, response); + }); + + if (!loginWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::Login: WriteBlock failed immediately"); + SetState(LoginState::Failed); + return false; + } + + // Start management timeout + StartLoginTimer(); + return true; +} + +// --------------------------------------------------------------------------- +// Logout +// --------------------------------------------------------------------------- + +bool SBP2LoginSession::Logout() noexcept { + if (state_ != LoginState::LoggedIn && state_ != LoginState::Suspended) { + ASFW_LOG(SBP2, "SBP2LoginSession::Logout: state=%s, ignoring", ToString(state_)); + return false; + } + + SetState(LoginState::LoggingOut); + BuildLogoutORB(); + + ASFW_LOG(SBP2, "SBP2LoginSession::Logout: sending logout ORB loginID=%u", loginID_); + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + const Async::FWAddress mgmtAddr{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = 0xFFFF, + .addressLo = ManagementAgentAddressLo(targetInfo_.managementAgentOffset), + .nodeID = loginNodeID_ + } + }; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + logoutWriteHandle_ = bus_.WriteBlock( + gen, node, mgmtAddr, + std::span{logoutORBAddressBE_.data(), logoutORBAddressBE_.size()}, + speed, + [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + OnLogoutWriteComplete(requestGeneration, status, response); + }); + + if (!logoutWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::Logout: WriteBlock failed"); + SetState(LoginState::Failed); + return false; + } + + StartLogoutTimer(); + return true; +} + +// --------------------------------------------------------------------------- +// Reconnect +// --------------------------------------------------------------------------- + +bool SBP2LoginSession::Reconnect() noexcept { + if (state_ != LoginState::Suspended && state_ != LoginState::LoggedIn) { + ASFW_LOG(SBP2, "SBP2LoginSession::Reconnect: state=%s, ignoring", ToString(state_)); + return false; + } + + SetState(LoginState::Reconnecting); + loginGeneration_ = static_cast(busInfo_.GetGeneration().value); + loginNodeID_ = targetInfo_.targetNodeId; + + if (!AllocateResources()) { + ASFW_LOG(SBP2, "SBP2LoginSession::Reconnect: resource allocation failed"); + SetState(LoginState::Failed); + return false; + } + + BuildReconnectORB(); + + ASFW_LOG(SBP2, + "SBP2LoginSession::Reconnect: sending reconnect ORB loginID=%u gen=%u", + loginID_, loginGeneration_); + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + const Async::FWAddress mgmtAddr{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = 0xFFFF, + .addressLo = ManagementAgentAddressLo(targetInfo_.managementAgentOffset), + .nodeID = loginNodeID_ + } + }; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + reconnectWriteHandle_ = bus_.WriteBlock( + gen, node, mgmtAddr, + std::span{reconnectORBAddressBE_.data(), reconnectORBAddressBE_.size()}, + speed, + [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + OnReconnectWriteComplete(requestGeneration, status, response); + }); + + if (!reconnectWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::Reconnect: WriteBlock failed, will retry"); + SubmitDelayedCallback(kLoginRetryDelayMs, [this]() { OnReconnectTimeout(); }); + reconnectTimerActive_ = true; + return true; // Will retry + } + + return true; +} + +// --------------------------------------------------------------------------- +// Bus Reset Handling +// --------------------------------------------------------------------------- + +void SBP2LoginSession::HandleBusReset(uint16_t newGeneration) noexcept { + ASFW_LOG(SBP2, + "SBP2LoginSession::HandleBusReset: state=%s newGen=%u loginGen=%u", + ToString(state_), newGeneration, loginGeneration_); + + switch (state_) { + case LoginState::LoggingIn: + // Login was in progress — cancel and retry after reset settles. + CancelLoginTimer(); + loginRetryCount_ = 0; + loginGeneration_ = newGeneration; + ClearORBTracking(true); + DeallocateResources(); + SetState(LoginState::Idle); + SubmitDelayedCallback(100, [this]() { (void)Login(); }); + break; + + case LoginState::LoggedIn: + // Transition to Suspended — wait for topology then reconnect. + CancelPendingTimer(); + ClearORBTracking(true); + DeallocateResources(); + SetState(LoginState::Suspended); + loginGeneration_ = newGeneration; + break; + + case LoginState::Reconnecting: + // Reconnect was in flight — retry. + reconnectTimerActive_ = false; + CancelPendingTimer(); + ClearORBTracking(true); + DeallocateResources(); + loginGeneration_ = newGeneration; + SetState(LoginState::Suspended); + SubmitDelayedCallback(100, [this]() { (void)Reconnect(); }); + break; + + case LoginState::LoggingOut: + // Logout in flight during bus reset — consider logged out. + CancelPendingTimer(); + DeallocateResources(); + ClearORBTracking(true); + SetState(LoginState::Idle); + break; + + case LoginState::Failed: + DeallocateResources(); + break; + + default: + break; + } +} + +// --------------------------------------------------------------------------- +// Accessors +// --------------------------------------------------------------------------- + +Async::FWAddress SBP2LoginSession::CommandBlockAgent() const noexcept { + return commandBlockAgent_; +} + +uint32_t SBP2LoginSession::ReconnectHoldSeconds() const noexcept { + return reconnectHold_ > 0 ? (1u << reconnectHold_) : 0; +} + +// --------------------------------------------------------------------------- +// Resource Allocation +// --------------------------------------------------------------------------- + +bool SBP2LoginSession::AllocateResources() noexcept { + const bool allResourcesAllocated = loginORBHandle_ != 0 && + loginResponseHandle_ != 0 && + statusBlockHandle_ != 0 && + reconnectORBHandle_ != 0 && + logoutORBHandle_ != 0; + const bool anyResourceAllocated = loginORBHandle_ != 0 || + loginResponseHandle_ != 0 || + statusBlockHandle_ != 0 || + reconnectORBHandle_ != 0 || + logoutORBHandle_ != 0; + + if (allResourcesAllocated) { + // Re-register callback in case it was previously cleared by a reset path. + if (statusBlockHandle_ != 0) { + addrSpaceMgr_.SetRemoteWriteCallback( + statusBlockHandle_, + [this](uint64_t /*handle*/, uint32_t offset, std::span payload) { + OnStatusBlockRemoteWrite(offset, payload); + }); + } + return true; // Already allocated + } + + if (anyResourceAllocated) { + ASFW_LOG(SBP2, + "SBP2LoginSession: inconsistent resource state before allocation " + "loginORB=0x%llu loginResp=0x%llu status=0x%llu reconnect=0x%llu logout=0x%llu", + static_cast(loginORBHandle_), + static_cast(loginResponseHandle_), + static_cast(statusBlockHandle_), + static_cast(reconnectORBHandle_), + static_cast(logoutORBHandle_)); + DeallocateResources(); + } + + if (!AllocateLoginORBAddressSpace()) { + return false; + } + if (!AllocateLoginResponseAddressSpace()) { + ASFW_LOG(SBP2, "SBP2LoginSession: rollback login resource allocation at login response stage"); + DeallocateResources(); + return false; + } + if (!AllocateStatusBlockAddressSpace()) { + ASFW_LOG(SBP2, "SBP2LoginSession: rollback login resource allocation at status block stage"); + DeallocateResources(); + return false; + } + if (!AllocateReconnectORBAddressSpace()) { + ASFW_LOG(SBP2, "SBP2LoginSession: rollback login resource allocation at reconnect ORB stage"); + DeallocateResources(); + return false; + } + if (!AllocateLogoutORBAddressSpace()) { + ASFW_LOG(SBP2, "SBP2LoginSession: rollback login resource allocation at logout ORB stage"); + DeallocateResources(); + return false; + } + + // Register a callback for status block writes — the device writes status + // here to signal login/reconnect/logout completion (and ORB completion + // in Step 2). + addrSpaceMgr_.SetRemoteWriteCallback( + statusBlockHandle_, + [this](uint64_t /*handle*/, uint32_t offset, std::span payload) { + OnStatusBlockRemoteWrite(offset, payload); + }); + + ASFW_LOG(SBP2, "SBP2LoginSession: all address spaces allocated"); + return true; +} + +void SBP2LoginSession::DeallocateResources() noexcept { + ClearORBTracking(true); + + if (loginORBHandle_) { + addrSpaceMgr_.DeallocateAddressRange(this, loginORBHandle_); + loginORBHandle_ = 0; + loginORBMeta_ = {}; + } + if (loginResponseHandle_) { + addrSpaceMgr_.DeallocateAddressRange(this, loginResponseHandle_); + loginResponseHandle_ = 0; + loginResponseMeta_ = {}; + } + if (statusBlockHandle_) { + addrSpaceMgr_.SetRemoteWriteCallback(statusBlockHandle_, {}); + addrSpaceMgr_.DeallocateAddressRange(this, statusBlockHandle_); + statusBlockHandle_ = 0; + statusBlockMeta_ = {}; + } + if (reconnectORBHandle_) { + addrSpaceMgr_.DeallocateAddressRange(this, reconnectORBHandle_); + reconnectORBHandle_ = 0; + reconnectORBMeta_ = {}; + } + if (logoutORBHandle_) { + addrSpaceMgr_.DeallocateAddressRange(this, logoutORBHandle_); + logoutORBHandle_ = 0; + logoutORBMeta_ = {}; + } + + loginORBAddressBE_ = {}; + reconnectORBAddressBE_ = {}; + logoutORBAddressBE_ = {}; +} + +bool SBP2LoginSession::AllocateLoginORBAddressSpace() noexcept { + // Login ORB is 32 bytes, readable by target device. + auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::LoginORB::kSize, + &loginORBHandle_, &loginORBMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession: failed to allocate login ORB address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(loginORBHandle_, "sbp2-login-orb"); + return true; +} + +bool SBP2LoginSession::AllocateLoginResponseAddressSpace() noexcept { + // Login response is 16 bytes, writable by target device. + auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::LoginResponse::kSize, + &loginResponseHandle_, &loginResponseMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession: failed to allocate login response address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(loginResponseHandle_, "sbp2-login-response"); + return true; +} + +bool SBP2LoginSession::AllocateStatusBlockAddressSpace() noexcept { + // Status block is up to 32 bytes, writable by target device. + auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::StatusBlock::kMaxSize, + &statusBlockHandle_, &statusBlockMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession: failed to allocate status block address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(statusBlockHandle_, "sbp2-status-fifo"); + return true; +} + +bool SBP2LoginSession::AllocateReconnectORBAddressSpace() noexcept { + auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::ReconnectORB::kSize, + &reconnectORBHandle_, &reconnectORBMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession: failed to allocate reconnect ORB address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(reconnectORBHandle_, "sbp2-reconnect-orb"); + return true; +} + +bool SBP2LoginSession::AllocateLogoutORBAddressSpace() noexcept { + auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::LogoutORB::kSize, + &logoutORBHandle_, &logoutORBMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession: failed to allocate logout ORB address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(logoutORBHandle_, "sbp2-logout-orb"); + return true; +} + +// --------------------------------------------------------------------------- +// ORB Construction +// --------------------------------------------------------------------------- + +void SBP2LoginSession::BuildLoginORB() noexcept { + std::memset(&loginORBBuffer_, 0, sizeof(loginORBBuffer_)); + + // Get local node ID for filling address fields. + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + + // Login response address: nodeID in upper 16 bits of addressHi. + const uint32_t responseAddrHi = ToBE32( + ComposeBusAddressHi(localNode, loginResponseMeta_.addressHi)); + const uint32_t responseAddrLo = ToBE32(loginResponseMeta_.addressLo); + + // Status FIFO address. + const uint32_t statusAddrHi = ToBE32( + ComposeBusAddressHi(localNode, statusBlockMeta_.addressHi)); + const uint32_t statusAddrLo = ToBE32(statusBlockMeta_.addressLo); + + // Fill login ORB fields. + loginORBBuffer_.loginResponseAddressHi = responseAddrHi; + loginORBBuffer_.loginResponseAddressLo = responseAddrLo; + loginORBBuffer_.options = static_cast( + Options::kLoginNotify | Options::kExclusiveLogin); + loginORBBuffer_.lun = ToBE16(targetInfo_.lun); + loginORBBuffer_.passwordLength = 0; + loginORBBuffer_.loginResponseLength = ToBE16(sizeof(Wire::LoginResponse)); + loginORBBuffer_.statusFIFOAddressHi = statusAddrHi; + loginORBBuffer_.statusFIFOAddressLo = statusAddrLo; + + // Write login ORB data to address space so device can read it. + addrSpaceMgr_.WriteLocalData( + this, loginORBHandle_, 0, + std::span{reinterpret_cast(&loginORBBuffer_), + sizeof(loginORBBuffer_)}); + + // Build the 8-byte management agent write payload: ORB address in big-endian. + // Format: [nodeID(2)][addressHi(2)][addressLo(4)] + loginORBAddressBE_[0] = static_cast(localNode >> 8); + loginORBAddressBE_[1] = static_cast(localNode & 0xFF); + loginORBAddressBE_[2] = static_cast(loginORBMeta_.addressHi >> 8); + loginORBAddressBE_[3] = static_cast(loginORBMeta_.addressHi & 0xFF); + const uint32_t orbAddrLoBE = ToBE32(loginORBMeta_.addressLo); + std::memcpy(&loginORBAddressBE_[4], &orbAddrLoBE, sizeof(uint32_t)); + + ASFW_LOG(SBP2, + "SBP2LoginSession::BuildLoginORB: mgmt=0x%08x payload=%02x%02x:%02x%02x:%02x%02x%02x%02x " + "ORB at %04x:%08x, response at %04x:%08x, status at %04x:%08x, LUN=%u", + ManagementAgentAddressLo(targetInfo_.managementAgentOffset), + loginORBAddressBE_[0], loginORBAddressBE_[1], + loginORBAddressBE_[2], loginORBAddressBE_[3], + loginORBAddressBE_[4], loginORBAddressBE_[5], + loginORBAddressBE_[6], loginORBAddressBE_[7], + localNode, loginORBMeta_.addressLo, + localNode, loginResponseMeta_.addressLo, + localNode, statusBlockMeta_.addressLo, + targetInfo_.lun); +} + +void SBP2LoginSession::BuildReconnectORB() noexcept { + std::memset(&reconnectORBBuffer_, 0, sizeof(reconnectORBBuffer_)); + + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + + // Reconnect ORB: options = reconnect (3) | notify + reconnectORBBuffer_.options = Options::kReconnectNotify; + reconnectORBBuffer_.loginID = ToBE16(loginID_); + + // Status FIFO address — reuse the dedicated status block address space. + const uint32_t statusAddrHi = ToBE32( + ComposeBusAddressHi(localNode, statusBlockMeta_.addressHi)); + const uint32_t statusAddrLo = ToBE32(statusBlockMeta_.addressLo); + reconnectORBBuffer_.statusFIFOAddressHi = statusAddrHi; + reconnectORBBuffer_.statusFIFOAddressLo = statusAddrLo; + + // Write reconnect ORB data. + addrSpaceMgr_.WriteLocalData( + this, reconnectORBHandle_, 0, + std::span{reinterpret_cast(&reconnectORBBuffer_), + sizeof(reconnectORBBuffer_)}); + + // Build management agent write payload. + reconnectORBAddressBE_[0] = static_cast(localNode >> 8); + reconnectORBAddressBE_[1] = static_cast(localNode & 0xFF); + reconnectORBAddressBE_[2] = static_cast(reconnectORBMeta_.addressHi >> 8); + reconnectORBAddressBE_[3] = static_cast(reconnectORBMeta_.addressHi & 0xFF); + const uint32_t addrLoBE = ToBE32(reconnectORBMeta_.addressLo); + std::memcpy(&reconnectORBAddressBE_[4], &addrLoBE, sizeof(uint32_t)); +} + +void SBP2LoginSession::BuildLogoutORB() noexcept { + std::memset(&logoutORBBuffer_, 0, sizeof(logoutORBBuffer_)); + + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + + logoutORBBuffer_.options = Options::kLogoutNotify; + logoutORBBuffer_.loginID = ToBE16(loginID_); + + // Status FIFO address — reuse the dedicated status block address space. + const uint32_t statusAddrHi = ToBE32( + ComposeBusAddressHi(localNode, statusBlockMeta_.addressHi)); + const uint32_t statusAddrLo = ToBE32(statusBlockMeta_.addressLo); + logoutORBBuffer_.statusFIFOAddressHi = statusAddrHi; + logoutORBBuffer_.statusFIFOAddressLo = statusAddrLo; + + addrSpaceMgr_.WriteLocalData( + this, logoutORBHandle_, 0, + std::span{reinterpret_cast(&logoutORBBuffer_), + sizeof(logoutORBBuffer_)}); + + logoutORBAddressBE_[0] = static_cast(localNode >> 8); + logoutORBAddressBE_[1] = static_cast(localNode & 0xFF); + logoutORBAddressBE_[2] = static_cast(logoutORBMeta_.addressHi >> 8); + logoutORBAddressBE_[3] = static_cast(logoutORBMeta_.addressHi & 0xFF); + const uint32_t addrLoBE = ToBE32(logoutORBMeta_.addressLo); + std::memcpy(&logoutORBAddressBE_[4], &addrLoBE, sizeof(uint32_t)); +} + +void SBP2LoginSession::RefreshCommandBlockAgentAddresses() noexcept { + commandBlockAgent_.nodeID = loginNodeID_; + fetchAgentAddress_ = Async::FWAddress{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = commandBlockAgent_.addressHi, + .addressLo = commandBlockAgent_.addressLo + Wire::CommandBlockAgentOffsets::kFetchAgent, + .nodeID = loginNodeID_ + } + }; + doorbellAddress_ = Async::FWAddress{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = commandBlockAgent_.addressHi, + .addressLo = commandBlockAgent_.addressLo + Wire::CommandBlockAgentOffsets::kDoorbell, + .nodeID = loginNodeID_ + } + }; + agentResetAddress_ = Async::FWAddress{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = commandBlockAgent_.addressHi, + .addressLo = commandBlockAgent_.addressLo + Wire::CommandBlockAgentOffsets::kAgentReset, + .nodeID = loginNodeID_ + } + }; + unsolicitedStatusAddress_ = Async::FWAddress{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = commandBlockAgent_.addressHi, + .addressLo = commandBlockAgent_.addressLo + Wire::CommandBlockAgentOffsets::kUnsolicitedStatusEnable, + .nodeID = loginNodeID_ + } + }; +} + +// --------------------------------------------------------------------------- +// Completion Handlers +// --------------------------------------------------------------------------- + +void SBP2LoginSession::OnLoginWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggingIn) { + return; + } + + CancelLoginTimer(); + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::OnLoginWriteComplete: status=%s, retrying (%u/%u)", + Async::ToString(status), loginRetryCount_ + 1, kLoginRetryMax); + + if (loginRetryCount_ < kLoginRetryMax) { + loginRetryCount_++; + SubmitDelayedCallback(kLoginRetryDelayMs, [this]() { + loginGeneration_ = static_cast(busInfo_.GetGeneration().value); + loginNodeID_ = targetInfo_.targetNodeId; + SetState(LoginState::Idle); + (void)Login(); + }); + return; + } + + ASFW_LOG(SBP2, "SBP2LoginSession: login retries exhausted"); + SetState(LoginState::Failed); + + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = -1; + params.generation = loginGeneration_; + loginCallback_(params); + } + return; + } + + // Management agent write ACK'd. The device will now: + // 1. Fetch the ORB via read from our address space + // 2. Process the login + // 3. Write login response to our address space + // 4. Write status block to our status FIFO + // + // We wait for the status block write callback (OnStatusBlockRemoteWrite) + // before reading the login response. Restart the timer for the device + // processing window. + ASFW_LOG(SBP2, "SBP2LoginSession: management agent write ACK'd, waiting for status block"); + StartLoginTimer(); +} + +void SBP2LoginSession::OnLoginTimeout() noexcept { + loginTimerActive_ = false; + + if (state_ != LoginState::LoggingIn) { + return; // Already handled + } + + ASFW_LOG(SBP2, + "SBP2LoginSession: login timeout (%u/%u) waiting for target node=0x%04x " + "to read label=sbp2-login-orb at %04x:%08x and write label=sbp2-status-fifo at %04x:%08x", + loginRetryCount_ + 1, + kLoginRetryMax, + loginNodeID_, + loginORBMeta_.addressHi, + loginORBMeta_.addressLo, + statusBlockMeta_.addressHi, + statusBlockMeta_.addressLo); + + if (loginRetryCount_ < kLoginRetryMax) { + loginRetryCount_++; + SetState(LoginState::Idle); + (void)Login(); + } else { + SetState(LoginState::Failed); + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = -2; // timeout + params.generation = loginGeneration_; + loginCallback_(params); + } + } +} + +void SBP2LoginSession::OnReconnectWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::Reconnecting) { + return; + } + + reconnectTimerActive_ = false; + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::OnReconnectWriteComplete: status=%s, retrying", + Async::ToString(status)); + + SubmitDelayedCallback(100, [this]() { (void)Reconnect(); }); + return; + } + + // Reconnect ORB write ACK'd. Wait for status block from device. + ASFW_LOG(SBP2, "SBP2LoginSession: reconnect write ACK'd, waiting for status block"); +} + +void SBP2LoginSession::OnReconnectTimeout() noexcept { + reconnectTimerActive_ = false; + + if (state_ != LoginState::Reconnecting) { + return; + } + + ASFW_LOG(SBP2, "SBP2LoginSession: reconnect timeout, falling back to full login"); + SetState(LoginState::Idle); + (void)Login(); +} + +void SBP2LoginSession::OnLogoutWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggingOut) { + return; + } + + logoutTimerActive_ = false; + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::OnLogoutWriteComplete: status=%s", + Async::ToString(status)); + + CancelPendingTimer(); + logoutTimerActive_ = false; + const uint16_t oldLoginID = loginID_; + loginID_ = 0; + SetState(LoginState::Idle); + + ASFW_LOG(SBP2, "SBP2LoginSession: logout transport failed (was loginID=%u)", oldLoginID); + + if (logoutCallback_) { + LogoutCompleteParams params{}; + params.status = -1; + params.generation = loginGeneration_; + logoutCallback_(params); + } + return; + } + + ASFW_LOG(SBP2, "SBP2LoginSession: logout management agent write ACK'd, waiting for status block"); +} + +void SBP2LoginSession::OnLogoutTimeout() noexcept { + logoutTimerActive_ = false; + if (state_ != LoginState::LoggingOut) { + return; + } + + ASFW_LOG(SBP2, "SBP2LoginSession: logout timeout, transitioning to Idle anyway"); + loginID_ = 0; + SetState(LoginState::Idle); + + if (logoutCallback_) { + LogoutCompleteParams params{}; + params.status = -2; + params.generation = loginGeneration_; + logoutCallback_(params); + } +} + +// --------------------------------------------------------------------------- +// Status Block Handling +// --------------------------------------------------------------------------- + +void SBP2LoginSession::OnStatusBlockRemoteWrite(uint32_t offset, + std::span payload) noexcept { + if (payload.empty()) { + return; + } + + Wire::StatusBlock block{}; + uint32_t len = static_cast(payload.size()); + if (len > sizeof(block)) { + len = sizeof(block); + } + std::memcpy(&block, payload.data(), len); + + ASFW_LOG(SBP2, + "SBP2LoginSession::OnStatusBlockRemoteWrite: state=%s offset=%u len=%u " + "src=%u resp=%u dead=%u sbpStatus=%u", + ToString(state_), offset, len, + block.Source(), block.Response(), block.DeadBit(), block.sbpStatus); + + // Dispatch to state-specific handler. + switch (state_) { + case LoginState::LoggingIn: + CancelLoginTimer(); + CompleteLoginFromStatusBlock(block, len); + break; + + case LoginState::Reconnecting: + reconnectTimerActive_ = false; + CompleteReconnectFromStatusBlock(block, len); + break; + + case LoginState::LoggingOut: + logoutTimerActive_ = false; + CancelPendingTimer(); + CompleteLogoutFromStatusBlock(block, len); + break; + + case LoginState::LoggedIn: + // Unsolicited status or ORB completion — forward to callback. + ProcessStatusBlock(block, len); + break; + + default: + ASFW_LOG(SBP2, "SBP2LoginSession: unexpected status block in state %s", ToString(state_)); + break; + } +} + +void SBP2LoginSession::CompleteLoginFromStatusBlock(const Wire::StatusBlock& block, + uint32_t length) noexcept { + if (block.sbpStatus != Wire::SBPStatus::kNoAdditionalInfo) { + ASFW_LOG(SBP2, + "SBP2LoginSession: login failed — sbpStatus=%u, retrying (%u/%u)", + block.sbpStatus, loginRetryCount_ + 1, kLoginRetryMax); + + if (loginRetryCount_ < kLoginRetryMax) { + loginRetryCount_++; + SubmitDelayedCallback(kLoginRetryDelayMs, [this]() { + loginGeneration_ = static_cast(busInfo_.GetGeneration().value); + loginNodeID_ = targetInfo_.targetNodeId; + SetState(LoginState::Idle); + (void)Login(); + }); + return; + } + + SetState(LoginState::Failed); + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = -1; + params.statusBlock = block; + params.statusBlockLength = length; + params.generation = loginGeneration_; + loginCallback_(params); + } + return; + } + + // Login succeeded — read the login response that the device wrote to + // our address space. + std::vector responseData; + auto kr = addrSpaceMgr_.ReadIncomingData( + this, loginResponseHandle_, 0, sizeof(Wire::LoginResponse), &responseData); + + if (kr != kIOReturnSuccess || responseData.size() < sizeof(Wire::LoginResponse)) { + ASFW_LOG(SBP2, + "SBP2LoginSession: failed to read login response (kr=0x%08x, len=%zu)", + kr, responseData.size()); + + if (loginRetryCount_ < kLoginRetryMax) { + loginRetryCount_++; + SubmitDelayedCallback(kLoginRetryDelayMs, [this]() { + loginGeneration_ = static_cast(busInfo_.GetGeneration().value); + loginNodeID_ = targetInfo_.targetNodeId; + SetState(LoginState::Idle); + (void)Login(); + }); + return; + } + + SetState(LoginState::Failed); + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = -1; + params.statusBlock = block; + params.statusBlockLength = length; + params.generation = loginGeneration_; + loginCallback_(params); + } + return; + } + + // Parse login response. + Wire::LoginResponse resp{}; + std::memcpy(&resp, responseData.data(), sizeof(resp)); + + loginID_ = FromBE16(resp.loginID); + reconnectHold_ = FromBE16(resp.reconnectHold); + loginResponse_ = resp; + + // Extract command block agent address. + const uint32_t cbaHi = FromBE32(resp.commandBlockAgentAddressHi); + const uint32_t cbaLo = FromBE32(resp.commandBlockAgentAddressLo); + commandBlockAgent_ = Async::FWAddress{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = static_cast(cbaHi & 0xFFFFu), + .addressLo = cbaLo, + .nodeID = loginNodeID_ + } + }; + + loginRetryCount_ = 0; + SetState(LoginState::LoggedIn); + + // Compute CBA-derived addresses for fetch agent, doorbell, agent reset, + // and unsolicited status enable. + RefreshCommandBlockAgentAddresses(); + + // If unsolicited status was requested while not logged in, enable it after + // all CBA-derived registers are available. + if (unsolicitedStatusRequested_) { + unsolicitedStatusRequested_ = false; + EnableUnsolicitedStatus(); + } + + ASFW_LOG(SBP2, + "SBP2LoginSession: login successful — loginID=%u, CBA=%04x:%08x, " + "reconnectHold=2^%u=%us", + loginID_, + commandBlockAgent_.addressHi, commandBlockAgent_.addressLo, + reconnectHold_, ReconnectHoldSeconds()); + + WriteBusyTimeout(); + + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = 0; + params.loginResponse = loginResponse_; + params.statusBlock = block; + params.statusBlockLength = length; + params.generation = loginGeneration_; + loginCallback_(params); + } +} + +void SBP2LoginSession::CompleteReconnectFromStatusBlock(const Wire::StatusBlock& block, + uint32_t length) noexcept { + if (block.sbpStatus != Wire::SBPStatus::kNoAdditionalInfo) { + ASFW_LOG(SBP2, + "SBP2LoginSession: reconnect failed — sbpStatus=%u, falling back to full login", + block.sbpStatus); + + SetState(LoginState::Idle); + (void)Login(); + return; + } + + SetState(LoginState::LoggedIn); + RefreshCommandBlockAgentAddresses(); + ASFW_LOG(SBP2, "SBP2LoginSession: reconnect successful — loginID=%u", loginID_); + + WriteBusyTimeout(); + + // If unsolicited status was requested while not logged in, enable it now. + if (unsolicitedStatusRequested_) { + unsolicitedStatusRequested_ = false; + EnableUnsolicitedStatus(); + } + + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = 0; + params.loginResponse = loginResponse_; + params.statusBlock = block; + params.statusBlockLength = length; + params.generation = loginGeneration_; + loginCallback_(params); + } +} + +void SBP2LoginSession::CompleteLogoutFromStatusBlock(const Wire::StatusBlock& block, + uint32_t length) noexcept { + const uint16_t oldLoginID = loginID_; + loginID_ = 0; + SetState(LoginState::Idle); + + ASFW_LOG(SBP2, "SBP2LoginSession: logout complete (was loginID=%u)", oldLoginID); + + if (logoutCallback_) { + LogoutCompleteParams params{}; + params.status = 0; + params.generation = loginGeneration_; + logoutCallback_(params); + } +} + +void SBP2LoginSession::ProcessStatusBlock(const Wire::StatusBlock& block, + uint32_t length) noexcept { + // Distinguish unsolicited vs solicited status. + // Unsolicited: (details & 0xC0) == 0x80 (source bit set, resp == 0) + const bool isUnsolicited = (block.details & 0xC0) == 0x80; + + if (statusCallback_) { + statusCallback_(block, length); + } + + if (isUnsolicited) { + // Re-enable unsolicited status so device can send more + EnableUnsolicitedStatus(); + return; + } + + const uint64_t orbKey = MakeORBKey(FromBE16(block.orbOffsetHi), FromBE32(block.orbOffsetLo)); + const auto it = outstandingORBs_.find(orbKey); + if (it == outstandingORBs_.end()) { + ASFW_LOG(SBP2, + "SBP2LoginSession::ProcessStatusBlock: unmatched ORB status hi=%04x lo=%08x", + FromBE16(block.orbOffsetHi), + FromBE32(block.orbOffsetLo)); + return; + } + + SBP2CommandORB* orb = it->second; + outstandingORBs_.erase(it); + if (orb != nullptr) { + orb->CancelTimer(); + auto& cb = orb->GetCompletionCallback(); + if (cb) { + cb(0, block.sbpStatus); + } + } +} + +// --------------------------------------------------------------------------- +// Internal Helpers +// --------------------------------------------------------------------------- + +void SBP2LoginSession::SetState(LoginState newState) noexcept { + if (state_ != newState) { + ASFW_LOG(SBP2, "SBP2LoginSession: state %s -> %s", ToString(state_), ToString(newState)); + state_ = newState; + } +} + +void SBP2LoginSession::StartLoginTimer() noexcept { + loginTimerActive_ = true; + SubmitDelayedCallback(targetInfo_.managementTimeoutMs, [this]() { + OnLoginTimeout(); + }); +} + +void SBP2LoginSession::StartLogoutTimer() noexcept { + logoutTimerActive_ = true; + SubmitDelayedCallback(targetInfo_.managementTimeoutMs, [this]() { + OnLogoutTimeout(); + }); +} + +void SBP2LoginSession::CancelLoginTimer() noexcept { + loginTimerActive_ = false; + CancelPendingTimer(); +} + +void SBP2LoginSession::CancelPendingTimer() noexcept { + delayedCallbackGeneration_.fetch_add(1, std::memory_order_acq_rel); +} + +void SBP2LoginSession::EnsureTimeoutQueue() noexcept { + if (timeoutQueue_ != nullptr || workQueue_ == nullptr) { + return; + } + +#ifdef ASFW_HOST_TEST + ownedTimeoutQueue_ = std::make_unique(); + if (ownedTimeoutQueue_ != nullptr && + workQueue_->UsesManualDispatchForTesting()) { + ownedTimeoutQueue_->SetManualDispatchForTesting(true); + } + timeoutQueue_ = ownedTimeoutQueue_.get(); +#else + IODispatchQueue* queue = nullptr; + const kern_return_t kr = IODispatchQueue::Create("com.asfw.sbp2.timeout", 0, 0, &queue); + if (kr != kIOReturnSuccess || queue == nullptr) { + ASFW_LOG(SBP2, + "SBP2LoginSession: failed to create timeout queue (kr=0x%08x), falling back to workQueue", + kr); + timeoutQueue_ = workQueue_; + return; + } + ownedTimeoutQueue_ = queue; + timeoutQueue_ = ownedTimeoutQueue_; +#endif +} + +void SBP2LoginSession::ReleaseOwnedTimeoutQueue() noexcept { +#ifdef ASFW_HOST_TEST + IODispatchQueue* ownedQueue = ownedTimeoutQueue_.get(); + ownedTimeoutQueue_.reset(); + if (timeoutQueue_ == ownedQueue) { + timeoutQueue_ = nullptr; + } +#else + IODispatchQueue* ownedQueue = ownedTimeoutQueue_; + if (ownedTimeoutQueue_ != nullptr) { + ownedTimeoutQueue_->release(); + ownedTimeoutQueue_ = nullptr; + } + if (timeoutQueue_ == ownedQueue) { + timeoutQueue_ = nullptr; + } +#endif +} + +IODispatchQueue* SBP2LoginSession::EffectiveTimeoutQueue() const noexcept { + if (timeoutQueue_ != nullptr) { + return timeoutQueue_; + } + return workQueue_; +} + +void SBP2LoginSession::SubmitDelayedCallback(uint64_t delayMs, + std::function callback) noexcept { + IODispatchQueue* delayQueue = timeoutQueue_ != nullptr ? timeoutQueue_ : workQueue_; + if (workQueue_ == nullptr || delayQueue == nullptr || !callback) { + return; + } + + const uint64_t expectedGeneration = + delayedCallbackGeneration_.fetch_add(1, std::memory_order_acq_rel) + 1ULL; + const std::weak_ptr weakLifetime = lifetimeToken_; + const uint64_t delayNs = delayMs * 1'000'000ULL; + IODispatchQueue* bounceQueue = workQueue_; + + DispatchAfterCompat(delayQueue, + delayNs, + [this, + weakLifetime, + expectedGeneration, + bounceQueue, + cb = std::move(callback)]() mutable { + if (weakLifetime.expired()) { + return; + } + DispatchAsyncCompat(bounceQueue, + [this, + weakLifetime, + expectedGeneration, + cb = std::move(cb)]() mutable { + if (weakLifetime.expired()) { + return; + } + if (delayedCallbackGeneration_.load(std::memory_order_acquire) != expectedGeneration) { + return; + } + cb(); + }); + }); +} + +uint64_t SBP2LoginSession::MakeORBKey(uint16_t addressHi, uint32_t addressLo) noexcept { + return (static_cast(addressHi) << 32) | static_cast(addressLo); +} + +uint64_t SBP2LoginSession::MakeORBKey(const Async::FWAddress& address) noexcept { + return MakeORBKey(address.addressHi, address.addressLo); +} + +void SBP2LoginSession::ClearORBTracking(bool cancelTimers) noexcept { + const bool shouldCancelFetchAgentWrite = + cancelTimers && fetchAgentWriteInUse_ && static_cast(fetchAgentWriteHandle_); + const Async::AsyncHandle fetchAgentWriteHandle = fetchAgentWriteHandle_; + const bool shouldCancelDoorbellWrite = + cancelTimers && doorbellInProgress_ && static_cast(doorbellWriteHandle_); + const Async::AsyncHandle doorbellWriteHandle = doorbellWriteHandle_; + + if (cancelTimers) { + for (auto& [key, orb] : outstandingORBs_) { + if (orb != nullptr) { + orb->CancelTimer(); + orb->SetAppended(false); + } + } + if (activeFetchAgentORB_ != nullptr) { + activeFetchAgentORB_->CancelTimer(); + } + for (auto* orb : pendingImmediateORBs_) { + if (orb != nullptr) { + orb->CancelTimer(); + } + } + } + + outstandingORBs_.clear(); + pendingImmediateORBs_.clear(); + chainTailORB_ = nullptr; + activeFetchAgentORB_ = nullptr; + fetchAgentWriteHandle_ = {}; + fetchAgentWriteInUse_ = false; + doorbellWriteHandle_ = {}; + doorbellInProgress_ = false; + doorbellRingAgain_ = false; + + if (shouldCancelFetchAgentWrite) { + (void)bus_.Cancel(fetchAgentWriteHandle); + } + if (shouldCancelDoorbellWrite) { + (void)bus_.Cancel(doorbellWriteHandle); + } +} + +// --------------------------------------------------------------------------- +// ORB Submission +// --------------------------------------------------------------------------- + +bool SBP2LoginSession::SubmitORB(SBP2CommandORB* orb) noexcept { + if (state_ != LoginState::LoggedIn) { + ASFW_LOG(SBP2, "SBP2LoginSession::SubmitORB: state=%s, rejecting", ToString(state_)); + return false; + } + + if (orb == nullptr || !orb->IsValid() || orb->IsAppended()) { + ASFW_LOG(SBP2, "SBP2LoginSession::SubmitORB: invalid ORB (null=%d, valid=%d, appended=%d)", + orb == nullptr, + orb != nullptr && orb->IsValid(), + orb != nullptr && orb->IsAppended()); + return false; + } + + // Fetch agent and doorbell addresses are computed at login time. + + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + const FW::FwSpeed speed = busInfo_.GetSpeed( + FW::NodeId{static_cast(loginNodeID_ & 0x3Fu)}); + + // Max payload log: derive from maxPayloadSize_ (bytes → log2(quadlets)). + // Capped at 15 (max 2^15 = 32768 quadlets = 128KB). + uint16_t maxPayloadLog = 0; + { + uint16_t payloadBytes = maxPayloadSize_; + if (payloadBytes > 4096) payloadBytes = 4096; + uint16_t quadlets = payloadBytes / 4; + if (quadlets > 0) { + maxPayloadLog = 0; + while ((1u << maxPayloadLog) < quadlets && maxPayloadLog < 15) { + maxPayloadLog++; + } + } + } + + const kern_return_t prepareKr = orb->PrepareForExecution(localNode, speed, maxPayloadLog); + if (prepareKr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::SubmitORB: PrepareForExecution failed: 0x%08x", + prepareKr); + return false; + } + + orb->SetFetchAgentWriteRetries(testFetchAgentWriteRetries_); + orb->SetAppended(true); + outstandingORBs_[MakeORBKey(orb->GetORBAddress())] = orb; + + const bool isImmediate = (orb->GetFlags() & SBP2CommandORB::kImmediate) != 0; + + if (isImmediate) { + chainTailORB_ = orb; + + if (fetchAgentWriteInUse_) { + pendingImmediateORBs_.push_back(orb); + ASFW_LOG(SBP2, "SBP2LoginSession::SubmitORB: fetch agent busy, deferring ORB"); + return true; + } + + return AppendORBImmediate(orb); + } else { + // Chained: link to last ORB, ring doorbell + if (!AppendORB(orb)) { + return false; + } + RingDoorbell(); + } + + return true; +} + +bool SBP2LoginSession::AppendORBImmediate(SBP2CommandORB* orb) noexcept { + if (orb == nullptr || fetchAgentWriteInUse_) { + return false; + } + + // Build 8-byte ORB address in big-endian + // Format: [nodeID(2)][addressHi(2)][addressLo(4)] + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + const Async::FWAddress orbAddr = orb->GetORBAddress(); + + fetchAgentWriteData_[0] = static_cast(localNode >> 8); + fetchAgentWriteData_[1] = static_cast(localNode & 0xFF); + fetchAgentWriteData_[2] = static_cast(orbAddr.addressHi >> 8); + fetchAgentWriteData_[3] = static_cast(orbAddr.addressHi & 0xFF); + const uint32_t addrLoBE = ToBE32(orbAddr.addressLo); + std::memcpy(&fetchAgentWriteData_[4], &addrLoBE, sizeof(uint32_t)); + + activeFetchAgentORB_ = orb; + fetchAgentWriteInUse_ = true; + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + fetchAgentWriteHandle_ = bus_.WriteBlock( + gen, node, fetchAgentAddress_, + std::span{fetchAgentWriteData_.data(), fetchAgentWriteData_.size()}, + speed, + [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + OnFetchAgentWriteComplete(requestGeneration, status, response); + }); + + if (!fetchAgentWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::AppendORBImmediate: WriteBlock failed"); + fetchAgentWriteInUse_ = false; + activeFetchAgentORB_ = nullptr; + FailSubmittedORB(orb, -1, Wire::SBPStatus::kUnspecifiedError); + return false; + } + + ASFW_LOG(SBP2, + "SBP2LoginSession::AppendORBImmediate: wrote ORB addr %04x:%08x to fetch agent", + localNode, orbAddr.addressLo); + return true; +} + +void SBP2LoginSession::FailActiveCommandIfPresent(int transportStatus, + uint8_t sbpStatus) noexcept { + if (activeFetchAgentORB_ == nullptr) { + return; + } + + if (activeCommandFailureCallback_) { + activeCommandFailureCallback_(transportStatus, sbpStatus); + } +} + +void SBP2LoginSession::FailSubmittedORB(SBP2CommandORB* orb, + int transportStatus, + uint8_t sbpStatus) noexcept { + if (orb == nullptr) { + return; + } + + const auto key = MakeORBKey(orb->GetORBAddress()); + outstandingORBs_.erase(key); + pendingImmediateORBs_.erase( + std::remove(pendingImmediateORBs_.begin(), pendingImmediateORBs_.end(), orb), + pendingImmediateORBs_.end()); + if (activeFetchAgentORB_ == orb) { + activeFetchAgentORB_ = nullptr; + } + if (chainTailORB_ == orb) { + chainTailORB_ = nullptr; + } + orb->CancelTimer(); + orb->SetAppended(false); + + auto& cb = orb->GetCompletionCallback(); + if (cb) { + cb(transportStatus, sbpStatus); + } +} + +void SBP2LoginSession::FailPendingImmediateORBs(int transportStatus, + uint8_t sbpStatus) noexcept { + auto pending = pendingImmediateORBs_; + for (auto* orb : pending) { + FailSubmittedORB(orb, transportStatus, sbpStatus); + } +} + +bool SBP2LoginSession::AppendORB(SBP2CommandORB* orb) noexcept { + if (chainTailORB_ == nullptr) { + // First ORB — write directly to fetch agent instead of chaining + chainTailORB_ = orb; + return AppendORBImmediate(orb); + } + + if (chainTailORB_ != orb) { + const Async::FWAddress orbAddr = orb->GetORBAddress(); + + // Set the new ORB's address in big-endian into the last ORB's next pointer + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + const uint32_t nextHi = ToBE32(ComposeBusAddressHi(localNode, orbAddr.addressHi)); + const uint32_t nextLo = ToBE32(orbAddr.addressLo); + const kern_return_t linkKr = chainTailORB_->SetNextORBAddress(nextHi, nextLo); + if (linkKr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::AppendORB: SetNextORBAddress failed: 0x%08x", + linkKr); + FailSubmittedORB(orb, -1, Wire::SBPStatus::kUnspecifiedError); + return false; + } + + chainTailORB_ = orb; + } + + return true; +} + +void SBP2LoginSession::RingDoorbell() noexcept { + if (doorbellInProgress_) { + doorbellRingAgain_ = true; + return; + } + + doorbellInProgress_ = true; + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + doorbellWriteHandle_ = bus_.WriteQuad( + gen, node, doorbellAddress_, 0, speed, + [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + OnDoorbellComplete(requestGeneration, status, response); + }); + + if (!doorbellWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::RingDoorbell: WriteQuad failed"); + doorbellInProgress_ = false; + } +} + +void SBP2LoginSession::OnFetchAgentWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggedIn) { + return; + } + + fetchAgentWriteInUse_ = false; + fetchAgentWriteHandle_ = {}; + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, + "SBP2LoginSession::OnFetchAgentWriteComplete: status=%s, retries=%u", + Async::ToString(status), + activeFetchAgentORB_ ? activeFetchAgentORB_->GetFetchAgentWriteRetries() : 0); + + if (activeFetchAgentORB_ != nullptr) { + uint32_t retries = activeFetchAgentORB_->GetFetchAgentWriteRetries(); + if (retries > 0) { + retries--; + activeFetchAgentORB_->SetFetchAgentWriteRetries(retries); + // Retry after a delay + SBP2CommandORB* retryORB = activeFetchAgentORB_; + SubmitDelayedCallback(1000, [this, retryORB]() { + if (activeFetchAgentORB_ == retryORB) { + AppendORBImmediate(retryORB); + } + }); + return; + } + + // Retries exhausted — report failure + SBP2CommandORB* failedORB = activeFetchAgentORB_; + if (failedORB != nullptr) { + FailActiveCommandIfPresent(-1, Wire::SBPStatus::kUnspecifiedError); + FailSubmittedORB(failedORB, -1, Wire::SBPStatus::kUnspecifiedError); + } + FailPendingImmediateORBs(-1, Wire::SBPStatus::kUnspecifiedError); + ClearORBTracking(true); + ResetFetchAgent(nullptr); + } + return; + } + + // Fetch agent write succeeded. Submit deferred ORB if any. + activeFetchAgentORB_ = nullptr; + + if (!pendingImmediateORBs_.empty()) { + SBP2CommandORB* next = pendingImmediateORBs_.front(); + pendingImmediateORBs_.pop_front(); + ASFW_LOG(SBP2, "SBP2LoginSession: submitting deferred ORB"); + AppendORBImmediate(next); + } +} + +void SBP2LoginSession::OnDoorbellComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggedIn) { + return; + } + + doorbellInProgress_ = false; + doorbellWriteHandle_ = {}; + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::OnDoorbellComplete: status=%s", + Async::ToString(status)); + } + + if (doorbellRingAgain_) { + doorbellRingAgain_ = false; + RingDoorbell(); + } +} + +// --------------------------------------------------------------------------- +// Management ORB Submission +// --------------------------------------------------------------------------- + +bool SBP2LoginSession::SubmitManagementORB(SBP2ManagementORB* orb) noexcept { + if (state_ != LoginState::LoggedIn) { + ASFW_LOG(SBP2, "SBP2LoginSession::SubmitManagementORB: state=%s, rejecting", + ToString(state_)); + return false; + } + + if (orb == nullptr) { + return false; + } + + EnsureTimeoutQueue(); + + // Configure the management ORB with current session parameters + orb->SetLoginID(loginID_); + orb->SetManagementAgentOffset(targetInfo_.managementAgentOffset); + orb->SetTargetNode(loginGeneration_, loginNodeID_); + orb->SetTimeout(targetInfo_.managementTimeoutMs); + orb->SetWorkQueue(workQueue_); + orb->SetTimeoutQueue(EffectiveTimeoutQueue()); + + ASFW_LOG(SBP2, "SBP2LoginSession::SubmitManagementORB: function=%u", + static_cast(orb->GetFunction())); + + return orb->Execute(); +} + +// --------------------------------------------------------------------------- +// Fetch Agent Reset +// --------------------------------------------------------------------------- + +void SBP2LoginSession::ResetFetchAgent(std::function callback) noexcept { + if (state_ != LoginState::LoggedIn) { + if (callback) callback(-1); + return; + } + + if (agentResetInProgress_) { + if (callback) callback(-1); + return; + } + + agentResetInProgress_ = true; + agentResetCallback_ = std::move(callback); + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + agentResetWriteHandle_ = bus_.WriteQuad( + gen, node, agentResetAddress_, 0, speed, + [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + OnAgentResetComplete(requestGeneration, status, response); + }); + + if (!agentResetWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::ResetFetchAgent: WriteQuad failed"); + agentResetInProgress_ = false; + if (agentResetCallback_) { + agentResetCallback_(-1); + agentResetCallback_ = nullptr; + } + } +} + +void SBP2LoginSession::OnAgentResetComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_) { + return; + } + + agentResetInProgress_ = false; + + // Clear ORB chain after reset + ClearORBTracking(true); + + ASFW_LOG(SBP2, "SBP2LoginSession::OnAgentResetComplete: status=%s, ORB chain cleared", + Async::ToString(status)); + + if (agentResetCallback_) { + int result = (status == Async::AsyncStatus::kSuccess) ? 0 : -1; + auto cb = std::move(agentResetCallback_); + agentResetCallback_ = nullptr; + cb(result); + } +} + +// --------------------------------------------------------------------------- +// Unsolicited Status Enable +// --------------------------------------------------------------------------- + +void SBP2LoginSession::EnableUnsolicitedStatus() noexcept { + if (state_ != LoginState::LoggedIn) { + unsolicitedStatusRequested_ = true; + return; + } + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + unsolicitedStatusWriteHandle_ = bus_.WriteQuad( + gen, node, unsolicitedStatusAddress_, 0, speed, + [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + OnUnsolicitedStatusEnableComplete(requestGeneration, status, response); + }); +} + +void SBP2LoginSession::OnUnsolicitedStatusEnableComplete( + uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggedIn) { + return; + } + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::OnUnsolicitedStatusEnableComplete: status=%s", + Async::ToString(status)); + } +} + +// --------------------------------------------------------------------------- +// Busy Timeout +// --------------------------------------------------------------------------- + +void SBP2LoginSession::WriteBusyTimeout() noexcept { + if (busyTimeoutInProgress_) { + bus_.Cancel(busyTimeoutWriteHandle_); + busyTimeoutInProgress_ = false; + } + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + const Async::FWAddress busyAddr{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = kCSRBusAddressHi, + .addressLo = kBusyTimeoutAddressLo, + .nodeID = loginNodeID_ + } + }; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + busyTimeoutInProgress_ = true; + busyTimeoutWriteHandle_ = bus_.WriteBlock( + gen, + node, + busyAddr, + std::span{reinterpret_cast(&busyTimeoutBuffer_), 4}, + speed, + [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + OnBusyTimeoutComplete(requestGeneration, status, response); + }); + + if (!busyTimeoutWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::WriteBusyTimeout: WriteBlock failed"); + busyTimeoutInProgress_ = false; + } +} + +void SBP2LoginSession::OnBusyTimeoutComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span) noexcept { + if (expectedGeneration != loginGeneration_) { + return; + } + + busyTimeoutInProgress_ = false; + + if (status != Async::AsyncStatus::kSuccess && status != Async::AsyncStatus::kAborted) { + ASFW_LOG(SBP2, + "SBP2LoginSession::OnBusyTimeoutComplete: status=%s", + Async::ToString(status)); + } +} + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp new file mode 100644 index 00000000..af4d4b3d --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp @@ -0,0 +1,486 @@ +#pragma once + +// SBP-2 Login/Reconnect/Logout state machine for ASFW. +// Ported from Apple IOFireWireSBP2Login.cpp — simplified for DriverKit. +// +// Lifecycle: +// 1. Create SBP2LoginSession with bus + address-space deps +// 2. Call Configure() with ROM-derived parameters (management offset, LUN, etc.) +// 3. Call Login() — sends login ORB to device's management agent +// 4. On success, session is kLoggedIn — can submit ORBs via fetch agent +// 5. On bus reset, auto Reconnect() with stored loginID +// 6. Call Logout() to terminate session + +#include "SBP2WireFormats.hpp" +#include "SBP2CommandORB.hpp" +#include "SBP2ManagementORB.hpp" +#include "AddressSpaceManager.hpp" +#include "../../Async/AsyncTypes.hpp" +#include "../../Logging/Logging.hpp" + +#include +#ifdef ASFW_HOST_TEST +#include "../../Testing/HostDriverKitStubs.hpp" +#else +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +namespace ASFW::Async { +class IFireWireBus; +class IFireWireBusInfo; +} + +namespace ASFW::Protocols::SBP2 { + +// --------------------------------------------------------------------------- +// Configuration parameters (from Config ROM Unit_Directory parsing) +// --------------------------------------------------------------------------- + +struct SBP2TargetInfo { + uint32_t managementAgentOffset{0}; // From Management_Agent_Offset key + uint16_t lun{0}; // Logical unit number + + // From Unit_Characteristics key (if present) + uint32_t managementTimeoutMs{2000}; // (byte[1] of unitCharacteristics) * 500 ms + uint16_t maxORBSize{32}; // (byte[0] * 4), min 32 + uint16_t maxCommandBlockSize{0}; // maxORBSize - sizeof(NormalORB header) + + // From Fast_Start key (optional) + bool fastStartSupported{false}; + uint8_t fastStartOffset{0}; + uint8_t fastStartMaxPayload{0}; + + // Target node (from discovery) + uint16_t targetNodeId{0xFFFF}; +}; + +// --------------------------------------------------------------------------- +// Login completion callback parameters +// --------------------------------------------------------------------------- + +struct LoginCompleteParams { + int status{0}; // 0 = success, negative = errno-style error + Wire::LoginResponse loginResponse{}; + Wire::StatusBlock statusBlock{}; + uint32_t statusBlockLength{0}; + uint16_t generation{0}; +}; + +struct LogoutCompleteParams { + int status{0}; + uint16_t generation{0}; +}; + +// --------------------------------------------------------------------------- +// Login session states +// --------------------------------------------------------------------------- + +enum class LoginState : uint8_t { + Idle, + LoggingIn, + LoggedIn, + Reconnecting, + LoggingOut, + Suspended, // Lost after bus reset, waiting for reconnect + Failed +}; + +[[nodiscard]] inline constexpr const char* ToString(LoginState s) noexcept { + switch (s) { + case LoginState::Idle: return "Idle"; + case LoginState::LoggingIn: return "LoggingIn"; + case LoginState::LoggedIn: return "LoggedIn"; + case LoginState::Reconnecting: return "Reconnecting"; + case LoginState::LoggingOut: return "LoggingOut"; + case LoginState::Suspended: return "Suspended"; + case LoginState::Failed: return "Failed"; + } + return "Unknown"; +} + +// --------------------------------------------------------------------------- +// SBP2LoginSession +// --------------------------------------------------------------------------- + +class SBP2LoginSession { + friend class SBP2SessionRegistry; + +public: + using LoginCallback = std::function; + using LogoutCallback = std::function; + using StatusCallback = std::function; + using ActiveCommandFailureCallback = + std::function; + + SBP2LoginSession(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + AddressSpaceManager& addrSpaceMgr); + ~SBP2LoginSession(); + + SBP2LoginSession(const SBP2LoginSession&) = delete; + SBP2LoginSession& operator=(const SBP2LoginSession&) = delete; + + // ----------------------------------------------------------------------- + // Configuration (call once before Login) + // ----------------------------------------------------------------------- + + /// Configure target parameters from Config ROM. Must be called before Login(). + void Configure(const SBP2TargetInfo& info) noexcept; + + /// Set login completion callback. + void SetLoginCallback(LoginCallback cb) noexcept { loginCallback_ = std::move(cb); } + + /// Set logout completion callback. + void SetLogoutCallback(LogoutCallback cb) noexcept { logoutCallback_ = std::move(cb); } + + /// Set status block notification callback (receives solicited + unsolicited status). + void SetStatusCallback(StatusCallback cb) noexcept { statusCallback_ = std::move(cb); } + + /// Set callback for unrecoverable active command failure in fetch-agent path. + void SetActiveCommandFailureCallback(ActiveCommandFailureCallback cb) noexcept { + activeCommandFailureCallback_ = std::move(cb); + } + + /// Bind the IODispatchQueue used for delayed callbacks (timers). + /// Must be called before Login() for timeout/retry support. + void SetWorkQueue(IODispatchQueue* queue) noexcept; + void SetTimeoutQueue(IODispatchQueue* queue) noexcept; + + // ----------------------------------------------------------------------- + // Session operations + // ----------------------------------------------------------------------- + + /// Initiate login to device. Completion via loginCallback_. + /// Returns false if already logged in or configuration missing. + [[nodiscard]] bool Login() noexcept; + + /// Initiate logout. Completion via logoutCallback_. + [[nodiscard]] bool Logout() noexcept; + + /// Reconnect after bus reset. Called automatically or manually. + [[nodiscard]] bool Reconnect() noexcept; + + /// Handle bus reset notification — transitions to Suspended if logged in. + void HandleBusReset(uint16_t newGeneration) noexcept; + + // ----------------------------------------------------------------------- + // Accessors + // ----------------------------------------------------------------------- + + [[nodiscard]] LoginState State() const noexcept { return state_; } + [[nodiscard]] uint16_t LoginID() const noexcept { return loginID_; } + [[nodiscard]] uint16_t Generation() const noexcept { return loginGeneration_; } + [[nodiscard]] const SBP2TargetInfo& TargetInfo() const noexcept { return targetInfo_; } + + /// Command Block Agent address (valid after successful login). + [[nodiscard]] Async::FWAddress CommandBlockAgent() const noexcept; + + /// Get negotiated reconnect hold time (seconds). + [[nodiscard]] uint32_t ReconnectHoldSeconds() const noexcept; + + /// Get max payload size for ORBs (bytes). + [[nodiscard]] uint16_t MaxPayloadSize() const noexcept { return maxPayloadSize_; } + + /// Set max payload size override (clipped by login response). + void SetMaxPayloadSize(uint16_t bytes) noexcept { maxPayloadSize_ = bytes; } + +#ifdef ASFW_HOST_TEST + /// Test hook: override fetch-agent retry budget for subsequently submitted ORBs. + void SetFetchAgentWriteRetriesForTesting(uint32_t retries) noexcept { + testFetchAgentWriteRetries_ = retries; + } + + /// Reset testing override for fetch-agent retries. + void ResetFetchAgentWriteRetriesForTesting() noexcept { + testFetchAgentWriteRetries_ = 20; + } +#endif + + // ----------------------------------------------------------------------- + // ORB submission + // ----------------------------------------------------------------------- + + /// Submit a Normal Command ORB to the device's fetch agent. + /// Requires LoggedIn state. ORB must be fully configured before calling. + [[nodiscard]] bool SubmitORB(SBP2CommandORB* orb) noexcept; + + /// Submit a management ORB (abort task, reset, etc). + /// Requires LoggedIn state. ORB must be fully configured before calling. + [[nodiscard]] bool SubmitManagementORB(SBP2ManagementORB* orb) noexcept; + + /// Reset the fetch agent. Clears ORB chain. Completion via callback. + void ResetFetchAgent(std::function callback) noexcept; + + /// Re-enable unsolicited status after device sends one. + void EnableUnsolicitedStatus() noexcept; + +private: + // ----------------------------------------------------------------------- + // Internal: resource allocation + // ----------------------------------------------------------------------- + + bool AllocateResources() noexcept; + void DeallocateResources() noexcept; + + bool AllocateLoginORBAddressSpace() noexcept; + bool AllocateLoginResponseAddressSpace() noexcept; + bool AllocateStatusBlockAddressSpace() noexcept; + bool AllocateReconnectORBAddressSpace() noexcept; + bool AllocateLogoutORBAddressSpace() noexcept; + + // ----------------------------------------------------------------------- + // Internal: ORB construction and submission + // ----------------------------------------------------------------------- + + void BuildLoginORB() noexcept; + void BuildReconnectORB() noexcept; + void BuildLogoutORB() noexcept; + void RefreshCommandBlockAgentAddresses() noexcept; + + // ----------------------------------------------------------------------- + // Internal: completion handlers + // ----------------------------------------------------------------------- + + void OnLoginWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + void OnLoginTimeout() noexcept; + void OnReconnectWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + void OnReconnectTimeout() noexcept; + void OnLogoutWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + void OnLogoutTimeout() noexcept; + + // ----------------------------------------------------------------------- + // Internal: status block handling + // ----------------------------------------------------------------------- + + /// Called by AddressSpaceManager remote-write callback when the device + /// writes a status block. Dispatches to the appropriate state handler. + void OnStatusBlockRemoteWrite(uint32_t offset, std::span payload) noexcept; + + /// Parse and dispatch a received status block. + void ProcessStatusBlock(const Wire::StatusBlock& block, uint32_t length) noexcept; + + // Internal: login/reconnect completion via status block + void CompleteLoginFromStatusBlock(const Wire::StatusBlock& block, uint32_t length) noexcept; + void CompleteReconnectFromStatusBlock(const Wire::StatusBlock& block, uint32_t length) noexcept; + void CompleteLogoutFromStatusBlock(const Wire::StatusBlock& block, uint32_t length) noexcept; + + // ----------------------------------------------------------------------- + // Internal: helpers + // ----------------------------------------------------------------------- + + void SetState(LoginState newState) noexcept; + void StartLoginTimer() noexcept; + void StartLogoutTimer() noexcept; + void CancelLoginTimer() noexcept; + + /// Submit a delayed callback via IOTimerDispatchSource. + void SubmitDelayedCallback(uint64_t delayMs, + std::function callback) noexcept; + + /// Cancel any pending timer callback. + void CancelPendingTimer() noexcept; + void EnsureTimeoutQueue() noexcept; + void ReleaseOwnedTimeoutQueue() noexcept; + [[nodiscard]] IODispatchQueue* EffectiveTimeoutQueue() const noexcept; + void ClearORBTracking(bool cancelTimers) noexcept; + [[nodiscard]] static uint64_t MakeORBKey(uint16_t addressHi, uint32_t addressLo) noexcept; + [[nodiscard]] static uint64_t MakeORBKey(const Async::FWAddress& address) noexcept; + + // ----------------------------------------------------------------------- + // Members + // ----------------------------------------------------------------------- + + Async::IFireWireBus& bus_; + Async::IFireWireBusInfo& busInfo_; + AddressSpaceManager& addrSpaceMgr_; + + // Configuration + SBP2TargetInfo targetInfo_{}; + bool configured_{false}; + uint16_t maxPayloadSize_{4096}; // default, clipped by login + + // Session state + LoginState state_{LoginState::Idle}; + uint16_t loginID_{0}; + uint16_t loginGeneration_{0}; + uint16_t loginNodeID_{0xFFFF}; + + // Login response data + Wire::LoginResponse loginResponse_{}; + Async::FWAddress commandBlockAgent_{}; + uint16_t reconnectHold_{0}; + + // Login retry state + uint32_t loginRetryCount_{0}; + static constexpr uint32_t kLoginRetryMax = 32; + static constexpr uint64_t kLoginRetryDelayMs = 1000; + + // ----------------------------------------------------------------------- + // Address space handles (from AddressSpaceManager) + // ----------------------------------------------------------------------- + + uint64_t loginORBHandle_{0}; + AddressSpaceManager::AddressRangeMeta loginORBMeta_{}; + Wire::LoginORB loginORBBuffer_{}; + + uint64_t loginResponseHandle_{0}; + AddressSpaceManager::AddressRangeMeta loginResponseMeta_{}; + + uint64_t statusBlockHandle_{0}; + AddressSpaceManager::AddressRangeMeta statusBlockMeta_{}; + + uint64_t reconnectORBHandle_{0}; + AddressSpaceManager::AddressRangeMeta reconnectORBMeta_{}; + Wire::ReconnectORB reconnectORBBuffer_{}; + + uint64_t logoutORBHandle_{0}; + AddressSpaceManager::AddressRangeMeta logoutORBMeta_{}; + Wire::LogoutORB logoutORBBuffer_{}; + + // ORB addresses as big-endian for management agent write (8 bytes each) + std::array loginORBAddressBE_{}; + std::array reconnectORBAddressBE_{}; + std::array logoutORBAddressBE_{}; + + // ----------------------------------------------------------------------- + // Async handles for in-flight operations + // ----------------------------------------------------------------------- + + Async::AsyncHandle loginWriteHandle_{}; + Async::AsyncHandle reconnectWriteHandle_{}; + Async::AsyncHandle logoutWriteHandle_{}; + + bool loginTimerActive_{false}; + bool reconnectTimerActive_{false}; + bool logoutTimerActive_{false}; + + // ----------------------------------------------------------------------- + // Callbacks + // ----------------------------------------------------------------------- + + LoginCallback loginCallback_; + LogoutCallback logoutCallback_; + StatusCallback statusCallback_; + ActiveCommandFailureCallback activeCommandFailureCallback_; + uint32_t testFetchAgentWriteRetries_{20}; + + // ----------------------------------------------------------------------- + // Timer infrastructure + // ----------------------------------------------------------------------- + + IODispatchQueue* workQueue_{nullptr}; + IODispatchQueue* timeoutQueue_{nullptr}; +#ifdef ASFW_HOST_TEST + std::unique_ptr ownedTimeoutQueue_{}; +#else + IODispatchQueue* ownedTimeoutQueue_{nullptr}; +#endif + std::atomic delayedCallbackGeneration_{0}; + std::shared_ptr lifetimeToken_{std::make_shared(0)}; + + // ----------------------------------------------------------------------- + // Constants + // ----------------------------------------------------------------------- + + static constexpr uint16_t kCSRBusAddressHi = 0x0000FFFFu; + static constexpr uint32_t kBusyTimeoutAddressLo = 0xF0000210u; + static constexpr uint32_t kBusyTimeoutValue = 0x0000000Fu; + + // ----------------------------------------------------------------------- + // Fetch Agent / Doorbell internals + // ----------------------------------------------------------------------- + + /// Write ORB address to fetch agent (CBA + kORBPointer). + bool AppendORBImmediate(SBP2CommandORB* orb) noexcept; + void FailActiveCommandIfPresent(int transportStatus, uint8_t sbpStatus) noexcept; + void FailSubmittedORB(SBP2CommandORB* orb, + int transportStatus, + uint8_t sbpStatus) noexcept; + void FailPendingImmediateORBs(int transportStatus, uint8_t sbpStatus) noexcept; + + /// Chain ORB to last ORB's next pointer. + [[nodiscard]] bool AppendORB(SBP2CommandORB* orb) noexcept; + + /// Ring doorbell (write quadlet to CBA + kDoorbell). + void RingDoorbell() noexcept; + + /// Fetch agent write completion handler. + void OnFetchAgentWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + + /// Doorbell write completion handler. + void OnDoorbellComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + + /// Fetch agent reset completion handler. + void OnAgentResetComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + + /// Unsolicited status enable completion handler. + void OnUnsolicitedStatusEnableComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + + /// Write the SBP-2 busy timeout CSR to the target. + void WriteBusyTimeout() noexcept; + + /// Busy timeout write completion handler. + void OnBusyTimeoutComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + + // Fetch agent state + Async::FWAddress fetchAgentAddress_{}; + Async::FWAddress doorbellAddress_{}; + Async::AsyncHandle fetchAgentWriteHandle_{}; + bool fetchAgentWriteInUse_{false}; + + // ORB chain state + SBP2CommandORB* chainTailORB_{nullptr}; + SBP2CommandORB* activeFetchAgentORB_{nullptr}; + std::deque pendingImmediateORBs_; + std::unordered_map outstandingORBs_; + + // Doorbell state + Async::AsyncHandle doorbellWriteHandle_{}; + bool doorbellInProgress_{false}; + bool doorbellRingAgain_{false}; + + // Fetch agent write data (8-byte BE ORB address) + std::array fetchAgentWriteData_{}; + + // Agent reset state + Async::FWAddress agentResetAddress_{}; + Async::AsyncHandle agentResetWriteHandle_{}; + bool agentResetInProgress_{false}; + std::function agentResetCallback_; + + // Unsolicited status enable state + Async::FWAddress unsolicitedStatusAddress_{}; + Async::AsyncHandle unsolicitedStatusWriteHandle_{}; + bool unsolicitedStatusRequested_{false}; + + // Busy timeout state + Async::AsyncHandle busyTimeoutWriteHandle_{}; + bool busyTimeoutInProgress_{false}; + uint32_t busyTimeoutBuffer_{Wire::ToBE32(kBusyTimeoutValue)}; +}; + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.cpp b/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.cpp index dd75eee3..767454eb 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.cpp @@ -1,4 +1,5 @@ // SBP-2 Management ORB implementation. +// Ported from Apple IOFireWireSBP2ManagementORB. // Ref: SBP-2 §6 (Task Management) #include "SBP2ManagementORB.hpp" @@ -12,6 +13,155 @@ namespace ASFW::Protocols::SBP2 { using namespace ASFW::Protocols::SBP2::Wire; +namespace { + +constexpr int kManagementTransportFailure = -1; +constexpr int kManagementTimeout = -2; +constexpr int kManagementMalformedStatus = -3; +constexpr int kManagementDeviceFailure = -4; + +using ManagementAsyncState = SBP2ManagementORB::AsyncState; + +Async::AsyncHandle TakeWriteHandle(const std::shared_ptr& state) noexcept { + return Async::AsyncHandle{state->writeHandleValue.exchange(0, std::memory_order_acq_rel)}; +} + +void ClearStatusBlockCallback(const std::shared_ptr& state) noexcept { + const uint64_t handle = state->statusBlockHandle.load(std::memory_order_acquire); + if (handle != 0 && state->addrMgr != nullptr) { + state->addrMgr->SetRemoteWriteCallback(handle, {}); + } +} + +bool CompleteAsyncOperation(const std::shared_ptr& state, + int status) noexcept { + if (!state->inProgress.exchange(false, std::memory_order_acq_rel)) { + return false; + } + + state->timerActive.store(false, std::memory_order_relaxed); + state->timerGeneration.fetch_add(1, std::memory_order_acq_rel); + ClearStatusBlockCallback(state); + + const Async::AsyncHandle pendingWrite = TakeWriteHandle(state); + if (pendingWrite && state->bus != nullptr) { + (void)state->bus->Cancel(pendingWrite); + } + + if (!state->destroyed.load(std::memory_order_acquire) && + state->completionCallback) { + state->completionCallback(status); + } + return true; +} + +void HandleStatusBlockWrite(const std::shared_ptr& state, + uint32_t offset, + std::span payload) noexcept { + if (!state->inProgress.load(std::memory_order_relaxed)) { + return; + } + + ASFW_LOG(SBP2, "SBP2ManagementORB: received status block (offset=%u len=%zu)", + offset, payload.size()); + + if (offset != 0 || payload.size() < 8 || payload.size() > Wire::StatusBlock::kMaxSize) { + (void)CompleteAsyncOperation(state, kManagementMalformedStatus); + return; + } + + Wire::StatusBlock block{}; + std::memcpy(&block, payload.data(), payload.size()); + + const uint16_t orbOffsetHi = FromBE16(block.orbOffsetHi); + const uint32_t orbOffsetLo = FromBE32(block.orbOffsetLo); + if (orbOffsetHi != state->expectedORBAddressHi.load(std::memory_order_acquire) || + orbOffsetLo != state->expectedORBAddressLo.load(std::memory_order_acquire)) { + ASFW_LOG(SBP2, + "SBP2ManagementORB: status block ORB mismatch expected=%04x:%08x got=%04x:%08x", + state->expectedORBAddressHi.load(std::memory_order_relaxed), + state->expectedORBAddressLo.load(std::memory_order_relaxed), + orbOffsetHi, + orbOffsetLo); + (void)CompleteAsyncOperation(state, kManagementMalformedStatus); + return; + } + + if (block.Response() != 0 || block.DeadBit() != 0) { + ASFW_LOG(SBP2, + "SBP2ManagementORB: device rejected management ORB resp=%u dead=%u status=%u", + block.Response(), + block.DeadBit(), + block.sbpStatus); + (void)CompleteAsyncOperation(state, kManagementDeviceFailure); + return; + } + + if (block.sbpStatus != Wire::SBPStatus::kNoAdditionalInfo) { + ASFW_LOG(SBP2, + "SBP2ManagementORB: management ORB completed with sbpStatus=%u", + block.sbpStatus); + (void)CompleteAsyncOperation(state, kManagementDeviceFailure); + return; + } + + (void)CompleteAsyncOperation(state, 0); +} + +void HandleTimeout(const std::shared_ptr& state) noexcept { + if (!state->inProgress.load(std::memory_order_relaxed)) { + return; + } + + ASFW_LOG(SBP2, "SBP2ManagementORB: timeout"); + (void)CompleteAsyncOperation(state, kManagementTimeout); +} + +void HandleWriteComplete(const std::shared_ptr& state, + Async::AsyncStatus status, + std::span /*response*/, + uint32_t timeoutMs, + IODispatchQueue* workQueue, + IODispatchQueue* timeoutQueue) noexcept { + state->writeHandleValue.store(0, std::memory_order_release); + + if (!state->inProgress.load(std::memory_order_relaxed)) { + return; + } + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, "SBP2ManagementORB::OnWriteComplete: status=%s", + Async::ToString(status)); + (void)CompleteAsyncOperation(state, kManagementTransportFailure); + return; + } + + state->timerActive.store(true, std::memory_order_relaxed); + ASFW_LOG(SBP2, "SBP2ManagementORB: mgmt agent ACK'd, waiting for status block (timeout=%ums)", + timeoutMs); + + if (workQueue == nullptr || timeoutQueue == nullptr || timeoutMs == 0) { + return; + } + + const uint64_t expectedGeneration = + state->timerGeneration.fetch_add(1, std::memory_order_acq_rel) + 1ULL; + const uint64_t delayNs = static_cast(timeoutMs) * 1'000'000ULL; + + DispatchAfterCompat(timeoutQueue, delayNs, [state, expectedGeneration, workQueue]() { + DispatchAsyncCompat(workQueue, [state, expectedGeneration]() { + if (state->timerGeneration.load(std::memory_order_acquire) != expectedGeneration || + !state->timerActive.load(std::memory_order_relaxed) || + !state->inProgress.load(std::memory_order_relaxed)) { + return; + } + HandleTimeout(state); + }); + }); +} + +} // namespace + // --------------------------------------------------------------------------- // Construction / Destruction // --------------------------------------------------------------------------- @@ -22,11 +172,19 @@ SBP2ManagementORB::SBP2ManagementORB(Async::IFireWireBus& bus, : bus_(bus) , busInfo_(busInfo) , addrMgr_(addrMgr) - , owner_(owner) {} + , owner_(owner) + , asyncState_(std::make_shared(&bus_, &addrMgr_)) {} SBP2ManagementORB::~SBP2ManagementORB() { - timerGeneration_.fetch_add(1, std::memory_order_acq_rel); - lifetimeToken_.reset(); + asyncState_->destroyed.store(true, std::memory_order_release); + asyncState_->inProgress.store(false, std::memory_order_relaxed); + asyncState_->timerActive.store(false, std::memory_order_relaxed); + asyncState_->timerGeneration.fetch_add(1, std::memory_order_acq_rel); + ClearStatusBlockCallback(asyncState_); + const Async::AsyncHandle pendingHandle = TakeWriteHandle(asyncState_); + if (pendingHandle) { + (void)bus_.Cancel(pendingHandle); + } DeallocateResources(); } @@ -35,8 +193,19 @@ SBP2ManagementORB::~SBP2ManagementORB() { // --------------------------------------------------------------------------- bool SBP2ManagementORB::AllocateResources() noexcept { - if (orbHandle_ != 0) { - return true; // Already allocated + const auto state = asyncState_; + const auto registerStatusWriteCallback = [this, state]() { + addrMgr_.SetRemoteWriteCallback( + statusBlockHandle_, + [state](uint64_t /*handle*/, uint32_t offset, std::span payload) { + HandleStatusBlockWrite(state, offset, payload); + }); + state->statusBlockHandle.store(statusBlockHandle_, std::memory_order_release); + }; + + if (orbHandle_ != 0 && statusBlockHandle_ != 0) { + registerStatusWriteCallback(); + return true; } // Allocate ORB address space (32 bytes) @@ -44,7 +213,7 @@ bool SBP2ManagementORB::AllocateResources() noexcept { owner_, 0xFFFF, Wire::TaskManagementORB::kSize, &orbHandle_, &orbMeta_); if (kr != kIOReturnSuccess) { - ASFW_LOG(Async, "SBP2ManagementORB: failed to allocate ORB: 0x%08x", kr); + ASFW_LOG(SBP2, "SBP2ManagementORB: failed to allocate ORB: 0x%08x", kr); return false; } @@ -53,18 +222,14 @@ bool SBP2ManagementORB::AllocateResources() noexcept { owner_, 0xFFFF, Wire::StatusBlock::kMaxSize, &statusBlockHandle_, &statusBlockMeta_); if (kr != kIOReturnSuccess) { - ASFW_LOG(Async, "SBP2ManagementORB: failed to allocate status block: 0x%08x", kr); + ASFW_LOG(SBP2, "SBP2ManagementORB: failed to allocate status block: 0x%08x", kr); addrMgr_.DeallocateAddressRange(owner_, orbHandle_); orbHandle_ = 0; return false; } // Register remote-write callback for the per-ORB status block - addrMgr_.SetRemoteWriteCallback( - statusBlockHandle_, - [this](uint64_t /*handle*/, uint32_t offset, std::span payload) { - OnStatusBlockWrite(offset, payload); - }); + registerStatusWriteCallback(); return true; } @@ -86,7 +251,11 @@ void SBP2ManagementORB::DeallocateResources() noexcept { // ORB construction // --------------------------------------------------------------------------- -void SBP2ManagementORB::BuildManagementORB() noexcept { +kern_return_t SBP2ManagementORB::BuildManagementORB() noexcept { + if (orbHandle_ == 0 || statusBlockHandle_ == 0) { + return kIOReturnNotReady; + } + std::memset(&orbBuffer_, 0, sizeof(orbBuffer_)); const uint16_t localNode = @@ -109,10 +278,14 @@ void SBP2ManagementORB::BuildManagementORB() noexcept { orbBuffer_.statusFIFOAddressLo = ToBE32(statusBlockMeta_.addressLo); // Write ORB to address space - addrMgr_.WriteLocalData( + const kern_return_t writeKr = addrMgr_.WriteLocalData( owner_, orbHandle_, 0, std::span{reinterpret_cast(&orbBuffer_), sizeof(orbBuffer_)}); + if (writeKr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2ManagementORB: failed to write management ORB: 0x%08x", writeKr); + return writeKr; + } // Build 8-byte management agent write payload: ORB address in BE orbAddressBE_[0] = static_cast(localNode >> 8); @@ -122,11 +295,12 @@ void SBP2ManagementORB::BuildManagementORB() noexcept { const uint32_t addrLoBE = ToBE32(orbMeta_.addressLo); std::memcpy(&orbAddressBE_[4], &addrLoBE, sizeof(uint32_t)); - ASFW_LOG(Async, + ASFW_LOG(SBP2, "SBP2ManagementORB: built function=%u loginID=%u ORB at %04x:%08x status at %04x:%08x", fn, loginID_, localNode, orbMeta_.addressLo, localNode, statusBlockMeta_.addressLo); + return kIOReturnSuccess; } // --------------------------------------------------------------------------- @@ -134,8 +308,8 @@ void SBP2ManagementORB::BuildManagementORB() noexcept { // --------------------------------------------------------------------------- bool SBP2ManagementORB::Execute() noexcept { - if (inProgress_.load(std::memory_order_relaxed)) { - ASFW_LOG(Async, "SBP2ManagementORB::Execute: already in progress"); + if (asyncState_->inProgress.load(std::memory_order_relaxed)) { + ASFW_LOG(SBP2, "SBP2ManagementORB::Execute: already in progress"); return false; } @@ -143,9 +317,17 @@ bool SBP2ManagementORB::Execute() noexcept { return false; } - BuildManagementORB(); + const kern_return_t buildKr = BuildManagementORB(); + if (buildKr != kIOReturnSuccess) { + return false; + } - inProgress_.store(true, std::memory_order_relaxed); + asyncState_->destroyed.store(false, std::memory_order_release); + asyncState_->completionCallback = completionCallback_; + asyncState_->expectedORBAddressHi.store(orbMeta_.addressHi, std::memory_order_release); + asyncState_->expectedORBAddressLo.store(orbMeta_.addressLo, std::memory_order_release); + asyncState_->timerActive.store(false, std::memory_order_relaxed); + asyncState_->inProgress.store(true, std::memory_order_relaxed); // Write ORB address to management agent const FW::Generation gen{generation_}; @@ -159,91 +341,28 @@ bool SBP2ManagementORB::Execute() noexcept { }; const FW::FwSpeed speed = busInfo_.GetSpeed(node); - writeHandle_ = bus_.WriteBlock( + const Async::AsyncHandle writeHandle = bus_.WriteBlock( gen, node, mgmtAddr, std::span{orbAddressBE_.data(), orbAddressBE_.size()}, speed, - [this](Async::AsyncStatus status, std::span response) { - OnWriteComplete(status, response); + [state = asyncState_, + timeoutMs = timeoutMs_, + workQueue = workQueue_, + effectiveTimeoutQueue = (timeoutQueue_ != nullptr ? timeoutQueue_ : workQueue_)]( + Async::AsyncStatus status, + std::span response) { + HandleWriteComplete(state, status, response, timeoutMs, workQueue, effectiveTimeoutQueue); }); - if (!writeHandle_) { - ASFW_LOG(Async, "SBP2ManagementORB::Execute: WriteBlock failed"); - inProgress_.store(false, std::memory_order_relaxed); + if (!writeHandle) { + ASFW_LOG(SBP2, "SBP2ManagementORB::Execute: WriteBlock failed"); + asyncState_->inProgress.store(false, std::memory_order_relaxed); return false; } + asyncState_->writeHandleValue.store(writeHandle.value, std::memory_order_release); - ASFW_LOG(Async, "SBP2ManagementORB::Execute: wrote management ORB to agent"); + ASFW_LOG(SBP2, "SBP2ManagementORB::Execute: wrote management ORB to agent"); return true; } -// --------------------------------------------------------------------------- -// Completion handlers -// --------------------------------------------------------------------------- - -void SBP2ManagementORB::OnWriteComplete(Async::AsyncStatus status, - std::span response) noexcept { - if (status != Async::AsyncStatus::kSuccess) { - ASFW_LOG(Async, "SBP2ManagementORB::OnWriteComplete: status=%s", - Async::ToString(status)); - Complete(-1); - return; - } - - // Management agent write ACK'd. Start timeout, wait for status block. - timerActive_.store(true, std::memory_order_relaxed); - ASFW_LOG(Async, "SBP2ManagementORB: mgmt agent ACK'd, waiting for status block (timeout=%ums)", - timeoutMs_); - - if (workQueue_ && timeoutMs_ > 0) { - const uint32_t timeout = timeoutMs_; - const uint64_t expectedGeneration = - timerGeneration_.fetch_add(1, std::memory_order_acq_rel) + 1ULL; - const std::weak_ptr weakLifetime = lifetimeToken_; - const uint64_t delayNs = static_cast(timeout) * 1'000'000ULL; - - DispatchAfterCompat(workQueue_, delayNs, [this, weakLifetime, expectedGeneration]() { - if (weakLifetime.expired()) { - return; - } - if (timerGeneration_.load(std::memory_order_acquire) != expectedGeneration || - !timerActive_.load(std::memory_order_relaxed) || - !inProgress_.load(std::memory_order_relaxed)) { - return; - } - OnTimeout(); - }); - } -} - -void SBP2ManagementORB::OnStatusBlockWrite(uint32_t offset, - std::span payload) noexcept { - if (!inProgress_.load(std::memory_order_relaxed)) { - return; - } - - ASFW_LOG(Async, "SBP2ManagementORB: received status block (offset=%u len=%zu)", - offset, payload.size()); - - Complete(0); -} - -void SBP2ManagementORB::OnTimeout() noexcept { - if (!inProgress_.load(std::memory_order_relaxed)) { - return; - } - ASFW_LOG(Async, "SBP2ManagementORB: timeout"); - Complete(-2); -} - -void SBP2ManagementORB::Complete(int status) noexcept { - inProgress_.store(false, std::memory_order_relaxed); - timerActive_.store(false, std::memory_order_relaxed); - timerGeneration_.fetch_add(1, std::memory_order_acq_rel); - - if (completionCallback_) { - completionCallback_(status); - } -} - } // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.hpp b/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.hpp index b7c01ee0..2d0dabe1 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.hpp @@ -4,6 +4,7 @@ // Written to the management agent address (same as login/reconnect/logout). // Has its own per-ORB status FIFO address space. // +// Ported from Apple IOFireWireSBP2ManagementORB. // Ref: SBP-2 §6 (Task Management) #include "AddressSpaceManager.hpp" @@ -18,7 +19,6 @@ #include #endif -#include #include #include #include @@ -60,31 +60,51 @@ class SBP2ManagementORB { } void SetManagementAgentOffset(uint32_t offset) noexcept { managementAgentOffset_ = offset; } void SetTimeout(uint32_t ms) noexcept { timeoutMs_ = ms; } - void SetCompletionCallback(CompletionCallback cb) noexcept { completionCallback_ = std::move(cb); } + void SetCompletionCallback(CompletionCallback cb) noexcept { + completionCallback_ = std::move(cb); + asyncState_->completionCallback = completionCallback_; + } - // Set node targeting before Execute. + // Set node targeting (called by SBP2LoginSession before Execute) void SetTargetNode(uint16_t generation, uint16_t nodeID) noexcept { generation_ = generation; nodeID_ = nodeID; } void SetWorkQueue(IODispatchQueue* queue) noexcept { workQueue_ = queue; } + void SetTimeoutQueue(IODispatchQueue* queue) noexcept { timeoutQueue_ = queue; } // Lifecycle [[nodiscard]] bool Execute() noexcept; [[nodiscard]] Function GetFunction() const noexcept { return function_; } - [[nodiscard]] bool InProgress() const noexcept { return inProgress_.load(std::memory_order_relaxed); } + [[nodiscard]] bool InProgress() const noexcept { + return asyncState_->inProgress.load(std::memory_order_relaxed); + } + + // Shared by delayed callbacks after the ORB object itself may have been destroyed. + struct AsyncState { + AsyncState(Async::IFireWireBus* busIn, AddressSpaceManager* addrMgrIn) + : bus(busIn) + , addrMgr(addrMgrIn) {} + + Async::IFireWireBus* bus{nullptr}; + AddressSpaceManager* addrMgr{nullptr}; + std::atomic destroyed{false}; + std::atomic inProgress{false}; + std::atomic timerActive{false}; + std::atomic timerGeneration{0}; + std::atomic statusBlockHandle{0}; + std::atomic writeHandleValue{0}; + std::atomic expectedORBAddressHi{0}; + std::atomic expectedORBAddressLo{0}; + CompletionCallback completionCallback{}; + }; private: bool AllocateResources() noexcept; void DeallocateResources() noexcept; - void BuildManagementORB() noexcept; - - void OnWriteComplete(Async::AsyncStatus status, std::span response) noexcept; - void OnStatusBlockWrite(uint32_t offset, std::span payload) noexcept; - void OnTimeout() noexcept; - void Complete(int status) noexcept; + [[nodiscard]] kern_return_t BuildManagementORB() noexcept; // Dependencies Async::IFireWireBus& bus_; @@ -114,11 +134,8 @@ class SBP2ManagementORB { // Management agent write payload (8-byte BE ORB address) std::array orbAddressBE_{}; - Async::AsyncHandle writeHandle_{}; - // State - std::atomic inProgress_{false}; - std::atomic timerActive_{false}; + std::shared_ptr asyncState_; // Node targeting uint16_t generation_{0}; @@ -126,8 +143,7 @@ class SBP2ManagementORB { // Timer infrastructure IODispatchQueue* workQueue_{nullptr}; - std::atomic timerGeneration_{0}; - std::shared_ptr lifetimeToken_{std::make_shared(0)}; + IODispatchQueue* timeoutQueue_{nullptr}; }; } // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp b/ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp index 9f16f490..4967a3e2 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp @@ -4,6 +4,7 @@ // Converts scatter-gather DMA segments into SBP-2 Page Table Entries (PTEs) // or a single direct-address descriptor when possible. // +// Ported from Apple IOFireWireSBP2ORB::setCommandBuffers. // Ref: SBP-2 §5.1.2 (Page Table Entry format) #include "AddressSpaceManager.hpp" @@ -111,7 +112,7 @@ class SBP2PageTable { owner_, 0xFFFF, ptSize, &pageTableHandle_, &pageTableMeta_); if (kr != kIOReturnSuccess) { - ASFW_LOG(Async, "SBP2PageTable: failed to allocate page table: 0x%08x", kr); + ASFW_LOG(SBP2, "SBP2PageTable: failed to allocate page table: 0x%08x", kr); return false; } @@ -120,7 +121,7 @@ class SBP2PageTable { reinterpret_cast(ptes.data()), ptSize); kr = addrMgr_.WriteLocalData(owner_, pageTableHandle_, 0, pteSpan); if (kr != kIOReturnSuccess) { - ASFW_LOG(Async, "SBP2PageTable: failed to write page table: 0x%08x", kr); + ASFW_LOG(SBP2, "SBP2PageTable: failed to write page table: 0x%08x", kr); Clear(); return false; } @@ -132,7 +133,7 @@ class SBP2PageTable { result_.options = Wire::Options::kPageTableUnrestricted; result_.isDirect = false; - ASFW_LOG(Async, "SBP2PageTable: built %u PTEs (%u bytes) at %04x:%08x", + ASFW_LOG(SBP2, "SBP2PageTable: built %u PTEs (%u bytes) at %04x:%08x", pteCount_, ptSize, pageTableMeta_.addressHi, pageTableMeta_.addressLo); return true; } diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp new file mode 100644 index 00000000..97d6a937 --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp @@ -0,0 +1,668 @@ +#include "SBP2SessionRegistry.hpp" + +#include "../../Async/Interfaces/IFireWireBusInfo.hpp" +#include "../../Discovery/FWDevice.hpp" +#include "../../Discovery/FWUnit.hpp" + +#include + +namespace ASFW::Protocols::SBP2 { + +namespace { + +class IOLockGuard { +public: + explicit IOLockGuard(IOLock* lock) : lock_(lock) { + if (lock_ != nullptr) { + IOLockLock(lock_); + } + } + + ~IOLockGuard() { + if (lock_ != nullptr) { + IOLockUnlock(lock_); + } + } + + IOLockGuard(const IOLockGuard&) = delete; + IOLockGuard& operator=(const IOLockGuard&) = delete; + +private: + IOLock* lock_{nullptr}; +}; + +constexpr uint8_t kInquiryOpcode = 0x12; + +uint32_t BuildCommandFlags(SCSI::DataDirection direction) { + uint32_t flags = SBP2CommandORB::kNotify | + SBP2CommandORB::kImmediate | + SBP2CommandORB::kNormalORB; + if (direction == SCSI::DataDirection::FromTarget) { + flags |= SBP2CommandORB::kDataFromTarget; + } + return flags; +} + +SBP2TargetInfo BuildTargetInfoFromUnit(const Discovery::FWUnit& unit) { + SBP2TargetInfo info{}; + + info.managementAgentOffset = unit.GetManagementAgentOffset().value_or(0); + info.lun = static_cast(unit.GetLUN().value_or(0) & 0xFFFF); + + if (auto uc = unit.GetUnitCharacteristics(); uc.has_value()) { + const uint32_t value = *uc; + const uint8_t orbSizeUnits = static_cast((value >> 24) & 0xFF); + const uint8_t timeoutUnits = static_cast((value >> 16) & 0xFF); + info.managementTimeoutMs = static_cast(timeoutUnits) * 500; + info.maxORBSize = std::max(static_cast(orbSizeUnits) * 4, 32); + } + info.maxCommandBlockSize = info.maxORBSize > Wire::NormalORB::kHeaderSize + ? static_cast(info.maxORBSize - Wire::NormalORB::kHeaderSize) + : 0; + + if (auto fastStart = unit.GetFastStart(); fastStart.has_value()) { + const uint32_t value = *fastStart; + info.fastStartSupported = true; + info.fastStartOffset = static_cast((value >> 8) & 0xFF); + info.fastStartMaxPayload = static_cast(value & 0xFF); + } + + if (auto device = unit.GetDevice(); device) { + info.targetNodeId = device->GetNodeID(); + } + + return info; +} + +} // namespace + +SBP2SessionRegistry::SBP2SessionRegistry(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + AddressSpaceManager& addrSpaceMgr, + Discovery::IDeviceManager& deviceManager, + IODispatchQueue* workQueue) + : bus_(bus) + , busInfo_(busInfo) + , addrSpaceMgr_(addrSpaceMgr) + , deviceManager_(deviceManager) + , workQueue_(workQueue) { + lock_ = IOLockAlloc(); +} + +SBP2SessionRegistry::~SBP2SessionRegistry() { + IOLockGuard lock(lock_); + for (auto& [handle, record] : sessions_) { + if (record.session && record.session->State() == LoginState::LoggedIn) { + (void)record.session->Logout(); + } + CleanupCommandResources(record); + CleanupManagementResources(record); + } + sessions_.clear(); + + if (lock_ != nullptr) { + IOLockFree(lock_); + lock_ = nullptr; + } +} + +std::expected SBP2SessionRegistry::CreateSession(void* owner, + uint64_t guid, + uint32_t romOffset) { + auto unit = ResolveUnit(guid, romOffset); + if (!unit) { + ASFW_LOG(SBP2, "SBP2SessionRegistry: no unit found for guid=0x%016llx romOffset=%u", + guid, romOffset); + return std::unexpected(kIOReturnNotFound); + } + + if (!unit->Matches(kSBP2UnitSpecId, kSBP2UnitSwVersion)) { + ASFW_LOG(SBP2, + "SBP2SessionRegistry: unit identity spec=0x%06x sw=0x%06x is not SBP-2", + unit->GetUnitSpecID(), unit->GetUnitSwVersion()); + return std::unexpected(kIOReturnUnsupported); + } + + const auto mgmtOffset = unit->GetManagementAgentOffset(); + if (!mgmtOffset.has_value() || *mgmtOffset == 0) { + ASFW_LOG(SBP2, "SBP2SessionRegistry: unit has no Management_Agent_Offset"); + return std::unexpected(kIOReturnUnsupported); + } + + auto targetInfo = BuildTargetInfoFromUnit(*unit); + if (targetInfo.managementAgentOffset == 0) { + return std::unexpected(kIOReturnUnsupported); + } + + auto session = std::make_unique(bus_, busInfo_, addrSpaceMgr_); + session->Configure(targetInfo); + session->SetWorkQueue(workQueue_); + + IOLockGuard lock(lock_); + const uint64_t handle = nextHandle_++; + session->SetActiveCommandFailureCallback([this, handle](int transportStatus, uint8_t sbpStatus) { + IOLockGuard cbLock(lock_); + auto* rec = FindByHandle(handle); + if (rec == nullptr) { + return; + } + + FailActiveCommandLocked(*rec, transportStatus, sbpStatus, false); + }); + + SBP2SessionRecord record{}; + record.handle = handle; + record.owner = owner; + record.guid = guid; + record.romOffset = romOffset; + record.session = std::move(session); + + auto [it, inserted] = sessions_.emplace(handle, std::move(record)); + if (!inserted) { + return std::unexpected(kIOReturnNoMemory); + } + + ASFW_LOG(SBP2, "SBP2SessionRegistry: created session handle=%llu guid=0x%016llx romOffset=%u", + handle, guid, romOffset); + return handle; +} + +bool SBP2SessionRegistry::StartLogin(uint64_t handle) { + IOLockGuard lock(lock_); + auto* record = FindByHandle(handle); + if (!record || !record->session) { + return false; + } + + if (record->session->State() != LoginState::Idle) { + return false; + } + + record->session->SetLoginCallback([this, handle](const LoginCompleteParams& params) { + IOLockGuard cbLock(lock_); + auto* rec = FindByHandle(handle); + if (rec == nullptr) { + return; + } + + rec->state.lastError = params.status; + if (params.status == 0) { + rec->state.loginID = params.loginResponse.loginID; + rec->state.loginState = LoginState::LoggedIn; + rec->state.generation = params.generation; + } else { + rec->state.loginState = LoginState::Failed; + } + }); + + return record->session->Login(); +} + +std::optional SBP2SessionRegistry::GetSessionState(uint64_t handle) const { + IOLockGuard lock(lock_); + const auto* record = FindByHandle(handle); + if (!record || !record->session) { + return std::nullopt; + } + + SBP2SessionState state{}; + state.loginState = record->session->State(); + state.loginID = record->session->LoginID(); + state.generation = record->session->Generation(); + state.lastError = record->state.lastError; + state.reconnectPending = (state.loginState == LoginState::Suspended); + return state; +} + +bool SBP2SessionRegistry::SubmitInquiry(uint64_t handle, uint8_t allocationLength) { + return SubmitCommand(handle, SCSI::BuildInquiryRequest(allocationLength)); +} + +std::optional> SBP2SessionRegistry::GetInquiryResult(uint64_t handle) { + IOLockGuard lock(lock_); + auto* record = FindByHandle(handle); + if (!record || !record->commandReady || !record->pendingCommandResult.has_value() || + record->lastCompletedCommandOpcode != kInquiryOpcode) { + return std::nullopt; + } + + if (record->pendingCommandResult->transportStatus != 0 || + record->pendingCommandResult->sbpStatus != Wire::SBPStatus::kNoAdditionalInfo) { + record->pendingCommandResult.reset(); + record->lastCompletedCommandOpcode.reset(); + record->commandReady = false; + return std::nullopt; + } + + auto payload = std::move(record->pendingCommandResult->payload); + record->pendingCommandResult.reset(); + record->lastCompletedCommandOpcode.reset(); + record->commandReady = false; + return payload; +} + +bool SBP2SessionRegistry::SubmitCommand(uint64_t handle, const SCSI::CommandRequest& request) { + IOLockGuard lock(lock_); + auto* record = FindByHandle(handle); + if (!record || !record->session || request.cdb.empty()) { + return false; + } + + if (record->session->State() != LoginState::LoggedIn || record->commandInFlight) { + return false; + } + + if (request.transferLength > 0 && request.direction == SCSI::DataDirection::None) { + return false; + } + if (request.direction == SCSI::DataDirection::FromTarget && !request.outgoingPayload.empty()) { + return false; + } + if (request.direction == SCSI::DataDirection::ToTarget && + request.outgoingPayload.size() != request.transferLength) { + return false; + } + + const uint16_t maxCDB = record->session->TargetInfo().maxCommandBlockSize; + if (maxCDB < request.cdb.size()) { + return false; + } + + uint64_t bufferHandle = 0; + AddressSpaceManager::AddressRangeMeta bufferMeta{}; + if (request.transferLength > 0) { + const kern_return_t kr = addrSpaceMgr_.AllocateAddressRangeAuto( + record->owner, 0xFFFF, request.transferLength, &bufferHandle, &bufferMeta); + if (kr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2SessionRegistry: failed to allocate command buffer: 0x%08x", kr); + return false; + } + } + + std::unique_ptr pageTable; + if (request.transferLength > 0) { + if (request.direction == SCSI::DataDirection::ToTarget) { + const kern_return_t writeKr = addrSpaceMgr_.WriteLocalData( + record->owner, + bufferHandle, + 0, + std::span{request.outgoingPayload.data(), request.outgoingPayload.size()}); + if (writeKr != kIOReturnSuccess) { + addrSpaceMgr_.DeallocateAddressRange(record->owner, bufferHandle); + return false; + } + } + + pageTable = std::make_unique(addrSpaceMgr_, record->owner); + SBP2PageTable::Segment segment{bufferMeta.address, request.transferLength}; + if (!pageTable->Build(std::span(&segment, 1), + busInfo_.GetLocalNodeID().value)) { + addrSpaceMgr_.DeallocateAddressRange(record->owner, bufferHandle); + return false; + } + } + + auto orb = std::make_unique(addrSpaceMgr_, record->owner, maxCDB); + orb->SetCommandBlock(std::span{request.cdb.data(), request.cdb.size()}); + orb->SetFlags(BuildCommandFlags(request.direction)); + orb->SetMaxPayloadSize(record->session->MaxPayloadSize()); + orb->SetTimeout(request.timeoutMs > 0 + ? request.timeoutMs + : record->session->TargetInfo().managementTimeoutMs); + if (pageTable) { + orb->SetDataDescriptor(pageTable->GetResult()); + } + + const uint64_t captureHandle = handle; + orb->SetCompletionCallback([this, captureHandle](int transportStatus, uint8_t sbpStatus) { + IOLockGuard cbLock(lock_); + auto* rec = FindByHandle(captureHandle); + if (rec == nullptr) { + return; + } + if (!rec->commandInFlight) { + return; + } + + rec->commandInFlight = false; + rec->commandReady = true; + rec->lastCompletedCommandOpcode = rec->activeCommandOpcode; + rec->activeCommandOpcode.reset(); + + SCSI::CommandResult result{}; + result.transportStatus = transportStatus; + result.sbpStatus = sbpStatus; + + if (transportStatus == 0 && + sbpStatus == Wire::SBPStatus::kNoAdditionalInfo && + rec->activeCommandRequest.has_value() && + rec->activeCommandRequest->direction == SCSI::DataDirection::FromTarget && + rec->activeCommandRequest->transferLength > 0 && + rec->commandBufferHandle != 0) { + std::vector payload; + const kern_return_t readKr = addrSpaceMgr_.ReadIncomingData( + rec->owner, + rec->commandBufferHandle, + 0, + rec->activeCommandRequest->transferLength, + &payload); + if (readKr == kIOReturnSuccess) { + result.payload = std::move(payload); + } else { + result.transportStatus = static_cast(readKr); + } + } + + if (rec->activeCommandRequest.has_value() && rec->activeCommandRequest->captureSenseData) { + result.senseData = result.payload; + } + + rec->state.lastError = static_cast(result.transportStatus); + if (result.transportStatus == 0 && result.sbpStatus == Wire::SBPStatus::kNoAdditionalInfo) { + rec->state.lastError = 0; + } + rec->pendingCommandResult = std::move(result); + rec->activeCommandRequest.reset(); + CleanupCommandResources(*rec); + }); + + if (!record->session->SubmitORB(orb.get())) { + if (bufferHandle != 0) { + addrSpaceMgr_.DeallocateAddressRange(record->owner, bufferHandle); + } + return false; + } + + record->commandInFlight = true; + record->commandReady = false; + record->pendingCommandResult.reset(); + record->activeCommandRequest = request; + record->activeCommandOpcode = request.cdb.front(); + record->commandBufferHandle = bufferHandle; + record->commandORB = std::move(orb); + record->commandPageTable = std::move(pageTable); + return true; +} + +std::optional SBP2SessionRegistry::GetCommandResult(uint64_t handle) { + IOLockGuard lock(lock_); + auto* record = FindByHandle(handle); + if (!record || !record->commandReady || !record->pendingCommandResult.has_value()) { + return std::nullopt; + } + + SCSI::CommandResult result = std::move(*record->pendingCommandResult); + record->pendingCommandResult.reset(); + record->lastCompletedCommandOpcode.reset(); + record->commandReady = false; + return result; +} + +bool SBP2SessionRegistry::SubmitTaskManagement(uint64_t handle, + SBP2ManagementORB::Function function) { + IOLockGuard lock(lock_); + auto* record = FindByHandle(handle); + if (!record || !record->session || !IsSupportedTaskManagementFunction(function)) { + return false; + } + + if (record->session->State() != LoginState::LoggedIn || record->managementORB) { + return false; + } + + auto orb = std::make_unique(bus_, busInfo_, addrSpaceMgr_, record->owner); + orb->SetFunction(function); + orb->SetLoginID(record->session->LoginID()); + orb->SetManagementAgentOffset(record->session->TargetInfo().managementAgentOffset); + orb->SetTimeout(record->session->TargetInfo().managementTimeoutMs); + orb->SetWorkQueue(workQueue_); + orb->SetTimeoutQueue(workQueue_); + orb->SetTargetNode(record->session->Generation(), record->session->TargetInfo().targetNodeId); + + const auto* submittedORB = orb.get(); + orb->SetCompletionCallback([this, handle, submittedORB, function](int status) { + IOLockGuard cbLock(lock_); + auto* rec = FindByHandle(handle); + if (rec == nullptr || rec->managementORB.get() != submittedORB) { + return; + } + + rec->state.lastError = static_cast(status); + if (status == 0 && IsSupportedTaskManagementFunction(function)) { + CleanupCommandResources(*rec); + rec->pendingCommandResult.reset(); + rec->activeCommandRequest.reset(); + rec->activeCommandOpcode.reset(); + rec->lastCompletedCommandOpcode.reset(); + rec->commandReady = false; + } + rec->managementORB.reset(); + }); + + record->managementORB = std::move(orb); + + if (!record->session->SubmitManagementORB(record->managementORB.get())) { + record->managementORB.reset(); + return false; + } + + return true; +} + +bool SBP2SessionRegistry::ReleaseSession(uint64_t handle) { + uint32_t waitMs = 0; + + { + IOLockGuard lock(lock_); + auto it = sessions_.find(handle); + if (it == sessions_.end()) { + return false; + } + + auto& record = it->second; + CleanupCommandResources(record); + CleanupManagementResources(record); + + if (record.session && + (record.session->State() == LoginState::LoggedIn || + record.session->State() == LoginState::Suspended)) { + waitMs = std::max(record.session->TargetInfo().managementTimeoutMs + 500, 500); + if (!record.session->Logout()) { + sessions_.erase(it); + return true; + } + } else { + sessions_.erase(it); + return true; + } + } + + for (uint32_t elapsed = 0; elapsed < waitMs; elapsed += 10) { + IOSleep(10); + + IOLockGuard lock(lock_); + auto it = sessions_.find(handle); + if (it == sessions_.end()) { + return true; + } + + auto& record = it->second; + const LoginState state = record.session ? record.session->State() : LoginState::Idle; + if (state != LoginState::LoggingOut) { + CleanupCommandResources(record); + sessions_.erase(it); + return true; + } + } + + IOLockGuard lock(lock_); + auto it = sessions_.find(handle); + if (it != sessions_.end()) { + CleanupCommandResources(it->second); + CleanupManagementResources(it->second); + sessions_.erase(it); + } + return true; +} + +void SBP2SessionRegistry::ReleaseOwner(void* owner) { + IOLockGuard lock(lock_); + for (auto it = sessions_.begin(); it != sessions_.end();) { + if (it->second.owner == owner) { + auto& record = it->second; + if (record.session && record.session->State() == LoginState::LoggedIn) { + (void)record.session->Logout(); + } + CleanupCommandResources(record); + CleanupManagementResources(record); + it = sessions_.erase(it); + } else { + ++it; + } + } +} + +void SBP2SessionRegistry::OnBusReset(uint16_t newGeneration) { + IOLockGuard lock(lock_); + for (auto& [handle, record] : sessions_) { + if (record.session) { + record.session->HandleBusReset(newGeneration); + } + CleanupManagementResources(record); + if (record.commandInFlight || record.commandORB) { + FailActiveCommandLocked(record, + static_cast(kIOReturnAborted), + Wire::SBPStatus::kRequestAborted, + true); + } + } +} + +void SBP2SessionRegistry::FailActiveCommandLocked(SBP2SessionRecord& record, + int transportStatus, + uint8_t sbpStatus, + bool keepSessionTracking) noexcept { + if (!record.commandInFlight) { + return; + } + + record.commandInFlight = false; + record.commandReady = true; + record.lastCompletedCommandOpcode = record.activeCommandOpcode; + record.activeCommandOpcode.reset(); + + SCSI::CommandResult result{}; + result.transportStatus = transportStatus; + result.sbpStatus = sbpStatus; + if (transportStatus == 0 && sbpStatus == Wire::SBPStatus::kNoAdditionalInfo) { + record.state.lastError = 0; + } else { + record.state.lastError = static_cast(result.transportStatus); + } + record.pendingCommandResult = std::move(result); + record.activeCommandRequest.reset(); + record.commandPageTable.reset(); + if (record.commandBufferHandle != 0) { + addrSpaceMgr_.DeallocateAddressRange(record.owner, record.commandBufferHandle); + record.commandBufferHandle = 0; + } + if (record.commandORB) { + record.commandORB->SetAppended(false); + } + if (!keepSessionTracking) { + return; + } + + CleanupCommandResources(record); +} + +void SBP2SessionRegistry::RefreshTargets(Discovery::Generation gen) { + IOLockGuard lock(lock_); + for (auto& [handle, record] : sessions_) { + if (!record.session || record.session->State() != LoginState::Suspended) { + continue; + } + + auto unit = ResolveUnit(record.guid, record.romOffset); + if (!unit) { + ASFW_LOG(SBP2, "SBP2SessionRegistry: RefreshTargets: unit not found for handle=%llu", + handle); + continue; + } + + auto targetInfo = BuildTargetInfoFromUnit(*unit); + record.session->Configure(targetInfo); + + ASFW_LOG(SBP2, "SBP2SessionRegistry: reconnecting session handle=%llu gen=%u", + handle, gen.value); + (void)record.session->Reconnect(); + } +} + +#ifdef ASFW_HOST_TEST +SBP2LoginSession* SBP2SessionRegistry::GetSessionForTesting(uint64_t handle) { + IOLockGuard lock(lock_); + auto* record = FindByHandle(handle); + if (!record) { + return nullptr; + } + return record->session.get(); +} +#endif + +SBP2SessionRecord* SBP2SessionRegistry::FindByHandle(uint64_t handle) { + auto it = sessions_.find(handle); + return it != sessions_.end() ? &it->second : nullptr; +} + +const SBP2SessionRecord* SBP2SessionRegistry::FindByHandle(uint64_t handle) const { + auto it = sessions_.find(handle); + return it != sessions_.end() ? &it->second : nullptr; +} + +std::shared_ptr SBP2SessionRegistry::ResolveUnit(uint64_t guid, + uint32_t romOffset) const { + const auto devices = deviceManager_.GetAllDevices(); + for (const auto& device : devices) { + if (!device || !device->IsReady() || device->GetGUID() != guid) { + continue; + } + for (const auto& unit : device->GetUnits()) { + if (unit && unit->GetDirectoryOffset() == romOffset) { + return unit; + } + } + } + return nullptr; +} + +void SBP2SessionRegistry::CleanupCommandResources(SBP2SessionRecord& record) { + if (record.session) { + record.session->ClearORBTracking(true); + } + if (record.commandBufferHandle != 0) { + addrSpaceMgr_.DeallocateAddressRange(record.owner, record.commandBufferHandle); + record.commandBufferHandle = 0; + } + record.commandORB.reset(); + record.commandPageTable.reset(); + record.commandInFlight = false; +} + +void SBP2SessionRegistry::CleanupManagementResources(SBP2SessionRecord& record) { + record.managementORB.reset(); +} + +bool SBP2SessionRegistry::IsSupportedTaskManagementFunction( + SBP2ManagementORB::Function function) noexcept { + switch (function) { + case SBP2ManagementORB::Function::AbortTaskSet: + case SBP2ManagementORB::Function::LogicalUnitReset: + case SBP2ManagementORB::Function::TargetReset: + return true; + default: + return false; + } +} + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp new file mode 100644 index 00000000..bc9c951f --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp @@ -0,0 +1,143 @@ +#pragma once + +// SBP-2 Session Registry — bridges discovery metadata to SBP2LoginSession instances. +// Owns sessions keyed by (guid, romOffset), handles bus-reset suspend/reconnect, +// and provides the INQUIRY command job for the v1 vertical slice. + +#include "SBP2LoginSession.hpp" +#include "SBP2CommandORB.hpp" +#include "SBP2ManagementORB.hpp" +#include "SBP2PageTable.hpp" +#include "SCSICommandSet.hpp" +#include "AddressSpaceManager.hpp" +#include "../../Discovery/IDeviceManager.hpp" +#include "../../Discovery/DiscoveryTypes.hpp" +#include "../../Logging/Logging.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ASFW::Protocols::SBP2 { + +// SBP-2 Unit_Directory identity used by Nikon/modernscan and visible in raw ROM. +inline constexpr uint32_t kSBP2UnitSpecId = 0x00609E; +inline constexpr uint32_t kSBP2UnitSwVersion = 0x010483; + +// Per-session state exposed to UserClient +struct SBP2SessionState { + LoginState loginState{LoginState::Idle}; + uint16_t loginID{0}; + uint16_t generation{0}; + int32_t lastError{0}; + bool reconnectPending{false}; +}; + +struct SBP2SessionRecord { + uint64_t handle{0}; + void* owner{nullptr}; + uint64_t guid{0}; + uint32_t romOffset{0}; + std::unique_ptr session; + SBP2SessionState state{}; + + std::optional activeCommandRequest; + std::optional pendingCommandResult; + std::optional activeCommandOpcode; + std::optional lastCompletedCommandOpcode; + bool commandReady{false}; + bool commandInFlight{false}; + std::unique_ptr commandORB; + std::unique_ptr commandPageTable; + uint64_t commandBufferHandle{0}; + + std::unique_ptr managementORB; +}; + +class SBP2SessionRegistry { +public: + SBP2SessionRegistry(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + AddressSpaceManager& addrSpaceMgr, + Discovery::IDeviceManager& deviceManager, + IODispatchQueue* workQueue = nullptr); + + ~SBP2SessionRegistry(); + + SBP2SessionRegistry(const SBP2SessionRegistry&) = delete; + SBP2SessionRegistry& operator=(const SBP2SessionRegistry&) = delete; + + // Create a session for (owner, guid, romOffset). + // Validates the unit is SBP-2 and has Management_Agent_Offset. + [[nodiscard]] std::expected CreateSession(void* owner, + uint64_t guid, + uint32_t romOffset); + + // Start login for a session. Returns false if not in Idle state. + [[nodiscard]] bool StartLogin(uint64_t handle); + + // Get session state. Returns nullopt if handle not found. + [[nodiscard]] std::optional GetSessionState(uint64_t handle) const; + + // Submit SCSI INQUIRY. Returns false if not logged in or inquiry already in-flight. + [[nodiscard]] bool SubmitInquiry(uint64_t handle, uint8_t allocationLength = 96); + + // Get inquiry result (destructive read). Returns nullopt if not ready. + [[nodiscard]] std::optional> GetInquiryResult(uint64_t handle); + + // Submit a generic SCSI command. Returns false if not logged in or another command is active. + [[nodiscard]] bool SubmitCommand(uint64_t handle, const SCSI::CommandRequest& request); + + // Get generic command result (destructive read). Returns nullopt if not ready. + [[nodiscard]] std::optional GetCommandResult(uint64_t handle); + + // Submit a task-management recovery ORB. + [[nodiscard]] bool SubmitTaskManagement(uint64_t handle, + SBP2ManagementORB::Function function); + + // Release a specific session. + [[nodiscard]] bool ReleaseSession(uint64_t handle); + + // Release all sessions for an owner (best-effort logout + cleanup). + void ReleaseOwner(void* owner); + + // Bus reset: suspend all active sessions. + void OnBusReset(uint16_t newGeneration); + + // After discovery completes: refresh target info and reconnect suspended sessions. + void RefreshTargets(Discovery::Generation gen); +#ifdef ASFW_HOST_TEST + // Testing hook: return the underlying login session for a handle. + SBP2LoginSession* GetSessionForTesting(uint64_t handle); +#endif + +private: + SBP2SessionRecord* FindByHandle(uint64_t handle); + const SBP2SessionRecord* FindByHandle(uint64_t handle) const; + void FailActiveCommandLocked(SBP2SessionRecord& record, + int transportStatus, + uint8_t sbpStatus, + bool keepSessionTracking) noexcept; + + std::shared_ptr ResolveUnit(uint64_t guid, uint32_t romOffset) const; + + void CleanupCommandResources(SBP2SessionRecord& record); + void CleanupManagementResources(SBP2SessionRecord& record); + [[nodiscard]] static bool IsSupportedTaskManagementFunction( + SBP2ManagementORB::Function function) noexcept; + + Async::IFireWireBus& bus_; + Async::IFireWireBusInfo& busInfo_; + AddressSpaceManager& addrSpaceMgr_; + Discovery::IDeviceManager& deviceManager_; + IODispatchQueue* workQueue_{nullptr}; + + IOLock* lock_{nullptr}; + std::map sessions_; + uint64_t nextHandle_{1}; +}; + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp b/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp index 12fa5f16..806df769 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp @@ -1,13 +1,15 @@ #pragma once // SBP-2 (Serial Bus Protocol 2) wire-format definitions. -// Based on ANSI INCITS 335-1999 (SBP-2). +// Based on ANSI INCITS 335-1999 (SBP-2) and Apple IOFireWireSBP2 structures. // All multi-byte fields are stored in **big-endian** (bus/wire) order. // Use ToBusOrder / FromBusOrder from Core/PhyPackets.hpp or std::byteswap for conversion. #include +#include #include #include +#include namespace ASFW::Protocols::SBP2::Wire { @@ -154,18 +156,25 @@ struct NormalORB { // Access via CommandBlock() helper. [[nodiscard]] uint32_t* CommandBlock() noexcept { - return reinterpret_cast(reinterpret_cast(this) + 16); + return reinterpret_cast(reinterpret_cast(this) + kHeaderSize); } [[nodiscard]] const uint32_t* CommandBlock() const noexcept { - return reinterpret_cast(reinterpret_cast(this) + 16); + return reinterpret_cast(reinterpret_cast(this) + kHeaderSize); } // Minimum ORB size (no command block) - static constexpr uint32_t kHeaderSize = 16; + static constexpr uint32_t kHeaderSize = 20; // Null next-ORB indicator (bit 31 set in hi address) static constexpr uint32_t kNextORBNull = 0x80000000u; }; +static_assert(sizeof(NormalORB) == NormalORB::kHeaderSize, "NormalORB header must be 20 bytes"); +static_assert(std::is_trivially_copyable_v, "NormalORB must stay trivially copyable"); +static_assert(offsetof(NormalORB, nextORBAddressHi) == 0, "NormalORB next hi offset changed"); +static_assert(offsetof(NormalORB, dataDescriptorHi) == 8, "NormalORB data descriptor hi offset changed"); +static_assert(offsetof(NormalORB, options) == 16, "NormalORB options offset changed"); +static_assert(offsetof(NormalORB, dataSize) == 18, "NormalORB data size offset changed"); + // Page Table Entry — maps data buffer for DMA. // Ref: SBP-2 §5.1.2 struct PageTableEntry { @@ -177,6 +186,10 @@ struct PageTableEntry { }; static_assert(sizeof(PageTableEntry) == PageTableEntry::kSize, "PTE must be 8 bytes"); +static_assert(std::is_trivially_copyable_v, + "PageTableEntry must stay trivially copyable"); +static_assert(offsetof(PageTableEntry, segmentLength) == 0, "PTE length offset changed"); +static_assert(offsetof(PageTableEntry, segmentBaseAddressLo) == 4, "PTE addressLo offset changed"); // --------------------------------------------------------------------------- // SBP-2 Status Block @@ -199,6 +212,11 @@ struct StatusBlock { }; static_assert(sizeof(StatusBlock) == 32, "StatusBlock must be 32 bytes"); +static_assert(std::is_trivially_copyable_v, + "StatusBlock must stay trivially copyable"); +static_assert(offsetof(StatusBlock, details) == 0, "StatusBlock details offset changed"); +static_assert(offsetof(StatusBlock, orbOffsetHi) == 2, "StatusBlock orbOffsetHi offset changed"); +static_assert(offsetof(StatusBlock, orbOffsetLo) == 4, "StatusBlock orbOffsetLo offset changed"); // --------------------------------------------------------------------------- // SBP-2 Management ORB (Task Management) @@ -219,6 +237,14 @@ struct TaskManagementORB { }; static_assert(sizeof(TaskManagementORB) == TaskManagementORB::kSize); +static_assert(std::is_trivially_copyable_v, + "TaskManagementORB must stay trivially copyable"); +static_assert(offsetof(TaskManagementORB, orbOffsetHi) == 0, + "TaskManagementORB orbOffsetHi offset changed"); +static_assert(offsetof(TaskManagementORB, options) == 16, + "TaskManagementORB options offset changed"); +static_assert(offsetof(TaskManagementORB, statusFIFOAddressHi) == 24, + "TaskManagementORB status FIFO hi offset changed"); // --------------------------------------------------------------------------- // Management Agent address calculation @@ -231,8 +257,8 @@ static_assert(sizeof(TaskManagementORB) == TaskManagementORB::kSize); } // SBP-2 ORBs embed a full 16-bit node ID in bus addresses. ASFW's generic -// bus-info path may expose only the local 6-bit physical node number, so expand -// it to the local-bus form (0xffc0 | phyId) before writing ORBs. +// bus-info path exposes only the local 6-bit physical node number, so expand it +// to the local-bus form Apple uses (0xffc0 | phyId) before writing ORBs. [[nodiscard]] inline constexpr uint16_t NormalizeBusNodeID(uint16_t nodeID) noexcept { if ((nodeID & 0xFFC0u) == 0xFFC0u) { return nodeID; @@ -247,6 +273,7 @@ static_assert(sizeof(TaskManagementORB) == TaskManagementORB::kSize); } // Command Block Agent register offsets (relative to agent base from login response). +// Verified against Apple IOFireWireSBP2Login::clearAllTasksInSet / login response processing. struct CommandBlockAgentOffsets { static constexpr uint32_t kAgentReset = 0x04; // Fetch agent reset (quadlet write) static constexpr uint32_t kFetchAgent = 0x08; // ORB pointer write (fetch agent, non-fast-start) diff --git a/ASFWDriver/Service/DriverContext.cpp b/ASFWDriver/Service/DriverContext.cpp index cef59fa5..5532f0f8 100644 --- a/ASFWDriver/Service/DriverContext.cpp +++ b/ASFWDriver/Service/DriverContext.cpp @@ -25,8 +25,10 @@ #include "../Hardware/HardwareInterface.hpp" #include "../Hardware/InterruptManager.hpp" #include "../Logging/Logging.hpp" +#include "../Protocols/AVC/AVCDiscovery.hpp" #include "../Protocols/AVC/FCPResponseRouter.hpp" #include "../Protocols/SBP2/AddressSpaceManager.hpp" +#include "../Protocols/SBP2/SBP2SessionRegistry.hpp" #include "../Scheduling/Scheduler.hpp" void ServiceContext::Reset() { @@ -44,12 +46,13 @@ void ServiceContext::Reset() { deps.configRomStager.reset(); deps.interrupts.reset(); deps.topology.reset(); - deps.fcpResponseRouter.reset(); // Clean up FCP router + deps.fcpResponseRouter.reset(); // Clean up FCP router deps.sbp2AddressSpaceManager.reset(); - deps.avcDiscovery.reset(); // Clean up AV/C discovery - deps.irmClient.reset(); // Clean up IRM client + deps.sbp2SessionRegistry.reset(); + deps.avcDiscovery.reset(); // Clean up AV/C discovery + deps.irmClient.reset(); // Clean up IRM client deps.asyncController.reset(); - deps.asyncSubsystem.reset(); // Stop and cleanup asyncSubsystem + deps.asyncSubsystem.reset(); // Stop and cleanup asyncSubsystem statusPublisher.Reset(); watchdog.Reset(); #ifndef ASFW_HOST_TEST @@ -137,116 +140,185 @@ void DriverWiring::EnsureDeps(ASFWDriver* driver, ::ServiceContext& ctx) { void DriverWiring::EnsureSbp2Deps(::ServiceContext& ctx) { auto& d = ctx.deps; + if (!d.fcpResponseRouter && d.avcDiscovery && ctx.controller) { + auto& bus = ctx.controller->Bus(); + d.fcpResponseRouter = std::make_shared( + *d.avcDiscovery, + bus + ); + ASFW_LOG(Controller, "[Controller] FCPResponseRouter initialized"); + } + if (!d.sbp2AddressSpaceManager && d.hardware) { - d.sbp2AddressSpaceManager = - std::make_shared(d.hardware.get()); + d.sbp2AddressSpaceManager = std::make_shared( + d.hardware.get()); ASFW_LOG(Controller, "[Controller] SBP2 AddressSpaceManager initialized"); } + if (!d.sbp2SessionRegistry && ctx.controller && d.deviceManager && d.sbp2AddressSpaceManager) { + auto& bus = ctx.controller->Bus(); + d.sbp2SessionRegistry = + std::make_shared( + bus, + bus, + *d.sbp2AddressSpaceManager, + *d.deviceManager, + ctx.workQueue.get()); + ASFW_LOG(Controller, "[Controller] SBP2 SessionRegistry initialized"); + } + if (ctx.controller) { ctx.controller->SetSbp2AddressSpaceManager(d.sbp2AddressSpaceManager); - } + ctx.controller->SetSBP2SessionRegistry(d.sbp2SessionRegistry); + } + + if (d.asyncSubsystem) { + if (auto* router = d.asyncSubsystem->GetPacketRouter()) { + auto* sbp2Manager = d.sbp2AddressSpaceManager.get(); + auto* fcpRouter = d.fcpResponseRouter.get(); + auto* responder = router->GetResponseSender(); + + // Quadlet write requests (tCode 0x0): direct address-space writes. + router->RegisterRequestHandler( + 0x0, + [sbp2Manager](const ASFW::Async::ARPacketView& packet) { + uint64_t destOffset = 0; + ASFW::Async::ResponseCode result = ASFW::Async::ResponseCode::AddressError; + if (!sbp2Manager || packet.header.size() < 16) { + return ASFW::Async::ResponseCode::AddressError; + } + + destOffset = ASFW::Async::ExtractDestOffset(packet.header); + const auto quadletData = + std::span(packet.header.data() + 12, 4); + result = sbp2Manager->ApplyRemoteWrite(destOffset, quadletData); + ASFW_LOG_V2( + Async, + "SBP2 AR write-quadlet req: src=0x%04x offset=0x%012llx len=4 -> rcode=0x%x", + packet.sourceID, + static_cast(destOffset), + static_cast(result)); + return result; + }); + + // Block write requests (tCode 0x1): SBP-2 first, then FCP fallback. + router->RegisterRequestHandler( + 0x1, + [sbp2Manager, fcpRouter](const ASFW::Async::ARPacketView& packet) { + uint64_t destOffset = 0; + if (sbp2Manager && packet.header.size() >= 16 && !packet.payload.empty()) { + destOffset = ASFW::Async::ExtractDestOffset(packet.header); + const auto sbp2Result = + sbp2Manager->ApplyRemoteWrite(destOffset, packet.payload); + ASFW_LOG_V2( + Async, + "SBP2 AR write-block req: src=0x%04x offset=0x%012llx len=%zu -> rcode=0x%x", + packet.sourceID, + static_cast(destOffset), + packet.payload.size(), + static_cast(sbp2Result)); + if (sbp2Result != ASFW::Async::ResponseCode::AddressError) { + return sbp2Result; + } + } + + if (fcpRouter) { + const Protocols::Ports::BlockWriteRequestView request{ + .sourceID = packet.sourceID, + .destOffset = ASFW::Async::ExtractDestOffset(packet.header), + .payload = packet.payload, + }; + const auto disposition = fcpRouter->RouteBlockWrite(request); + if (disposition == + Protocols::Ports::BlockWriteDisposition::kAddressError) { + return ASFW::Async::ResponseCode::AddressError; + } + return ASFW::Async::ResponseCode::Complete; + } - if (!d.asyncSubsystem) { - return; - } - - auto* router = d.asyncSubsystem->GetPacketRouter(); - if (!router) { - return; - } - - auto* sbp2Manager = d.sbp2AddressSpaceManager.get(); - auto* fcpRouter = d.fcpResponseRouter.get(); - auto* responder = router->GetResponseSender(); - - router->RegisterRequestHandler( - 0x0, - [sbp2Manager](const ASFW::Async::ARPacketView& packet) { - if (!sbp2Manager || packet.header.size() < 16) { - return ASFW::Async::ResponseCode::AddressError; - } - - const uint64_t destOffset = ASFW::Async::ExtractDestOffset(packet.header); - const auto quadletData = std::span(packet.header.data() + 12, 4); - return sbp2Manager->ApplyRemoteWrite(destOffset, quadletData); - }); - - router->RegisterRequestHandler( - 0x1, - [sbp2Manager, fcpRouter](const ASFW::Async::ARPacketView& packet) { - if (sbp2Manager && packet.header.size() >= 16 && !packet.payload.empty()) { - const uint64_t destOffset = ASFW::Async::ExtractDestOffset(packet.header); - const auto sbp2Result = - sbp2Manager->ApplyRemoteWrite(destOffset, packet.payload); - if (sbp2Result != ASFW::Async::ResponseCode::AddressError) { - return sbp2Result; - } - } - - if (fcpRouter) { - const ASFW::Protocols::Ports::BlockWriteRequestView request{ - .sourceID = packet.sourceID, - .destOffset = ASFW::Async::ExtractDestOffset(packet.header), - .payload = packet.payload, - }; - const auto disposition = fcpRouter->RouteBlockWrite(request); - if (disposition == ASFW::Protocols::Ports::BlockWriteDisposition::kAddressError) { return ASFW::Async::ResponseCode::AddressError; - } - return ASFW::Async::ResponseCode::Complete; - } - - return ASFW::Async::ResponseCode::AddressError; - }); - - router->RegisterRequestHandler( - 0x4, - [sbp2Manager, responder](const ASFW::Async::ARPacketView& packet) { - uint32_t quadlet = 0; - auto result = ASFW::Async::ResponseCode::AddressError; - - if (sbp2Manager && packet.header.size() >= 12) { - const uint64_t destOffset = ASFW::Async::ExtractDestOffset(packet.header); - result = sbp2Manager->ReadQuadlet(destOffset, &quadlet); - } - - if (responder) { - responder->SendReadQuadletResponse(packet, result, quadlet); - } - return ASFW::Async::ResponseCode::NoResponse; - }); - - router->RegisterRequestHandler( - 0x5, - [sbp2Manager, responder](const ASFW::Async::ARPacketView& packet) { - auto result = ASFW::Async::ResponseCode::AddressError; - ASFW::Protocols::SBP2::AddressSpaceManager::ReadSlice slice{}; - - if (sbp2Manager && packet.header.size() >= 16) { - const uint64_t destOffset = ASFW::Async::ExtractDestOffset(packet.header); - const auto readLength = - static_cast(ASFW::Async::ExtractDataLength(packet.header)); - if (readLength > 0) { - result = sbp2Manager->ResolveReadSlice(destOffset, readLength, &slice); - } else { - result = ASFW::Async::ResponseCode::DataError; - } - } - - if (responder) { - if (result == ASFW::Async::ResponseCode::Complete) { - responder->SendReadBlockResponse( - packet, result, slice.payloadDeviceAddress, slice.payloadLength); - } else { - responder->SendReadBlockResponse(packet, result, 0, 0); - } - } - return ASFW::Async::ResponseCode::NoResponse; - }); - - ASFW_LOG(Controller, - "[Controller] PacketRouter wired for SBP2 address spaces (tCode 0x0/0x1/0x4/0x5)"); + }); + + // Read quadlet requests (tCode 0x4): active read response from address-space manager. + router->RegisterRequestHandler( + 0x4, + [sbp2Manager, responder](const ASFW::Async::ARPacketView& packet) { + uint32_t quadlet = 0; + ASFW::Async::ResponseCode result = ASFW::Async::ResponseCode::AddressError; + + if (sbp2Manager && packet.header.size() >= 12) { + const uint64_t destOffset = ASFW::Async::ExtractDestOffset(packet.header); + result = sbp2Manager->ReadQuadlet(destOffset, &quadlet); + } + + if (responder) { + responder->SendReadQuadletResponse(packet, result, quadlet); + } + return ASFW::Async::ResponseCode::NoResponse; + }); + + // Read block requests (tCode 0x5): active read response from address-space manager. + router->RegisterRequestHandler( + 0x5, + [sbp2Manager, responder](const ASFW::Async::ARPacketView& packet) { + ASFW::Async::ResponseCode result = ASFW::Async::ResponseCode::AddressError; + ASFW::Protocols::SBP2::AddressSpaceManager::ReadSlice slice{}; + uint64_t destOffset = 0; + uint32_t readLength = 0; + + if (sbp2Manager && packet.header.size() >= 16) { + destOffset = ASFW::Async::ExtractDestOffset(packet.header); + readLength = + static_cast(ASFW::Async::ExtractDataLength(packet.header)); + if (readLength > 0) { + result = sbp2Manager->ResolveReadSlice(destOffset, readLength, &slice); + } else { + result = ASFW::Async::ResponseCode::DataError; + } + } + + if (result == ASFW::Async::ResponseCode::Complete) { + ASFW_LOG_V2( + Async, + "SBP2 AR read-block req: src=0x%04x offset=0x%012llx len=%u -> " + "rcode=0x%x payload=0x%08x/%u", + packet.sourceID, + static_cast(destOffset), + readLength, + static_cast(result), + static_cast(slice.payloadDeviceAddress), + slice.payloadLength); + } else { + ASFW_LOG_V2( + Async, + "SBP2 AR read-block req: src=0x%04x offset=0x%012llx len=%u -> " + "rcode=0x%x", + packet.sourceID, + static_cast(destOffset), + readLength, + static_cast(result)); + } + + if (responder) { + if (result == ASFW::Async::ResponseCode::Complete) { + responder->SendReadBlockResponse( + packet, + result, + slice.payloadDeviceAddress, + slice.payloadLength); + } else { + responder->SendReadBlockResponse(packet, result, 0, 0); + } + } + + return ASFW::Async::ResponseCode::NoResponse; + }); + + ASFW_LOG( + Controller, + "[Controller] PacketRouter wired for SBP2/FCP handlers (tCode 0x0/0x1/0x4/0x5)"); + } + } } kern_return_t DriverWiring::PrepareQueue(ASFWDriver& service, ::ServiceContext& ctx) { diff --git a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp index ec7f5b69..ad4d4a83 100644 --- a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp +++ b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp @@ -58,6 +58,16 @@ enum { kMethodDeallocateAddressRange = 47, kMethodReadIncomingData = 48, kMethodWriteLocalData = 49, + // SBP-2 session management + kMethodCreateSBP2Session = 53, + kMethodStartSBP2Login = 54, + kMethodGetSBP2SessionState = 55, + kMethodSubmitSBP2Inquiry = 56, + kMethodGetSBP2InquiryResult = 57, + kMethodReleaseSBP2Session = 58, + kMethodSubmitSBP2Command = 59, + kMethodGetSBP2CommandResult = 60, + kMethodSubmitSBP2TaskManagement = 61, // TODO(ASFW-IRM): Remove temporary IRM test method after dedicated validation tooling exists. kMethodTestIRMAllocation = 26, kMethodTestIRMRelease = 27, @@ -205,6 +215,8 @@ kern_return_t IMPL(ASFWDriverUserClient, Stop) { ivars->driver = nullptr; } if (auto* runtimeState = ASFW::UserClient::GetRuntimeState(this); runtimeState != nullptr) { + // Release owner-bound SBP-2 resources before handler teardown so + // abrupt client exit cannot strand address ranges inside the driver. runtimeState->ReleaseOwner(this); runtimeState->ResetHandlers(); } @@ -494,6 +506,34 @@ kern_return_t ASFWDriverUserClient::ExternalMethod(uint64_t selector, case kMethodStopIsochTransmit: return runtimeState->Isoch().StopIsochTransmit(arguments); + // SBP-2 session management (53-61) + case kMethodCreateSBP2Session: + return runtimeState->SBP2().CreateSBP2Session(arguments, this); + + case kMethodStartSBP2Login: + return runtimeState->SBP2().StartSBP2Login(arguments); + + case kMethodGetSBP2SessionState: + return runtimeState->SBP2().GetSBP2SessionState(arguments); + + case kMethodSubmitSBP2Inquiry: + return runtimeState->SBP2().SubmitSBP2Inquiry(arguments); + + case kMethodGetSBP2InquiryResult: + return runtimeState->SBP2().GetSBP2InquiryResult(arguments); + + case kMethodReleaseSBP2Session: + return runtimeState->SBP2().ReleaseSBP2Session(arguments, this); + + case kMethodSubmitSBP2Command: + return runtimeState->SBP2().SubmitSBP2Command(arguments); + + case kMethodGetSBP2CommandResult: + return runtimeState->SBP2().GetSBP2CommandResult(arguments); + + case kMethodSubmitSBP2TaskManagement: + return runtimeState->SBP2().SubmitSBP2TaskManagement(arguments); + default: return kIOReturnBadArgument; } diff --git a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.iig b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.iig index 599ca8e7..49502ff1 100644 --- a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.iig +++ b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.iig @@ -59,6 +59,16 @@ public: kMethodDeallocateAddressRange = 47, kMethodReadIncomingData = 48, kMethodWriteLocalData = 49, + // SBP-2 session management + kMethodCreateSBP2Session = 53, + kMethodStartSBP2Login = 54, + kMethodGetSBP2SessionState = 55, + kMethodSubmitSBP2Inquiry = 56, + kMethodGetSBP2InquiryResult = 57, + kMethodReleaseSBP2Session = 58, + kMethodSubmitSBP2Command = 59, + kMethodGetSBP2CommandResult = 60, + kMethodSubmitSBP2TaskManagement = 61, // TODO(ASFW-IRM): Remove temporary IRM test method after dedicated validation tooling exists. kMethodTestIRMAllocation = 26, kMethodTestIRMRelease = 27, diff --git a/ASFWDriver/UserClient/Core/UserClientRuntimeState.hpp b/ASFWDriver/UserClient/Core/UserClientRuntimeState.hpp index 666ed035..b9f59ada 100644 --- a/ASFWDriver/UserClient/Core/UserClientRuntimeState.hpp +++ b/ASFWDriver/UserClient/Core/UserClientRuntimeState.hpp @@ -51,10 +51,11 @@ class UserClientRuntimeState final { auto* controllerCore = GetControllerCorePtr(driver); auto* avcDiscovery = controllerCore ? controllerCore->GetAVCDiscovery() : nullptr; - auto* sbp2Manager = controllerCore ? controllerCore->GetSbp2AddressSpaceManager() : nullptr; + auto* sbp2Mgr = controllerCore ? controllerCore->GetSbp2AddressSpaceManager() : nullptr; + auto* sbp2Registry = controllerCore ? controllerCore->GetSBP2SessionRegistry() : nullptr; avcHandler_ = std::make_unique(avcDiscovery); isochHandler_ = std::make_unique(driver); - sbp2Handler_ = std::make_unique(sbp2Manager); + sbp2Handler_ = std::make_unique(sbp2Mgr, sbp2Registry); return HandlersReady(); } @@ -72,7 +73,10 @@ class UserClientRuntimeState final { } void ReleaseOwner(void* owner) noexcept { - if (owner != nullptr && sbp2Handler_ != nullptr) { + if (owner == nullptr) { + return; + } + if (sbp2Handler_ != nullptr) { sbp2Handler_->ReleaseOwner(owner); } } diff --git a/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp b/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp index 78e424a5..e76311b1 100644 --- a/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp +++ b/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -8,19 +9,24 @@ #include "../../Logging/Logging.hpp" #include "../../Protocols/SBP2/AddressSpaceManager.hpp" +#include "../../Protocols/SBP2/SCSICommandSet.hpp" +#include "../../Protocols/SBP2/SBP2SessionRegistry.hpp" +#include "../WireFormats/SBP2CommandWireFormats.hpp" namespace ASFW::UserClient { class SBP2Handler { public: - explicit SBP2Handler(ASFW::Protocols::SBP2::AddressSpaceManager* manager) - : manager_(manager) {} + explicit SBP2Handler(ASFW::Protocols::SBP2::AddressSpaceManager* manager, + ASFW::Protocols::SBP2::SBP2SessionRegistry* registry) + : manager_(manager), registry_(registry) {} ~SBP2Handler() = default; SBP2Handler(const SBP2Handler&) = delete; SBP2Handler& operator=(const SBP2Handler&) = delete; + // Address space management (selectors 46-49) kern_return_t AllocateAddressRange(IOUserClientMethodArguments* args, void* owner) { if (!manager_) { return kIOReturnNotReady; @@ -78,14 +84,34 @@ class SBP2Handler { std::vector data; const kern_return_t kr = manager_->ReadIncomingData(owner, handle, offset, length, &data); if (kr != kIOReturnSuccess) { + ASFW_LOG(UserClient, + "ReadIncomingData: owner=%p handle=0x%llx offset=%u len=%u -> kr=0x%x", + owner, + static_cast(handle), + offset, + length, + static_cast(kr)); return kr; } OSData* output = OSData::withBytes(data.data(), static_cast(data.size())); if (!output) { + ASFW_LOG(UserClient, + "ReadIncomingData: owner=%p handle=0x%llx offset=%u len=%u -> no memory", + owner, + static_cast(handle), + offset, + length); return kIOReturnNoMemory; } + ASFW_LOG(UserClient, + "ReadIncomingData: owner=%p handle=0x%llx offset=%u len=%u -> %u bytes", + owner, + static_cast(handle), + offset, + length, + static_cast(data.size())); args->structureOutput = output; args->structureOutputDescriptor = nullptr; return kIOReturnSuccess; @@ -128,10 +154,257 @@ class SBP2Handler { if (manager_) { manager_->ReleaseOwner(owner); } + if (registry_) { + registry_->ReleaseOwner(owner); + } + } + + // Session management (selectors 53-58) + + kern_return_t CreateSBP2Session(IOUserClientMethodArguments* args, void* owner) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 3 || + !args->scalarOutput || args->scalarOutputCount < 1) { + return kIOReturnBadArgument; + } + + const uint32_t guidHi = static_cast(args->scalarInput[0] & 0xFFFF'FFFFu); + const uint32_t guidLo = static_cast(args->scalarInput[1] & 0xFFFF'FFFFu); + const uint32_t romOffset = static_cast(args->scalarInput[2] & 0xFFFF'FFFFu); + const uint64_t guid = (static_cast(guidHi) << 32) | guidLo; + + auto result = registry_->CreateSession(owner, guid, romOffset); + if (!result.has_value()) { + return result.error(); + } + + args->scalarOutput[0] = *result; + args->scalarOutputCount = 1; + return kIOReturnSuccess; + } + + kern_return_t StartSBP2Login(IOUserClientMethodArguments* args) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 1) { + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + return registry_->StartLogin(handle) ? kIOReturnSuccess : kIOReturnError; + } + + kern_return_t GetSBP2SessionState(IOUserClientMethodArguments* args) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 1) { + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + auto state = registry_->GetSessionState(handle); + if (!state.has_value()) { + return kIOReturnNotFound; + } + + // Return as scalars: loginState, loginID, generation, lastError, reconnectPending + if (args->scalarOutput && args->scalarOutputCount >= 5) { + args->scalarOutput[0] = static_cast(state->loginState); + args->scalarOutput[1] = static_cast(state->loginID); + args->scalarOutput[2] = static_cast(state->generation); + args->scalarOutput[3] = static_cast(static_cast(state->lastError)); + args->scalarOutput[4] = state->reconnectPending ? 1 : 0; + args->scalarOutputCount = 5; + } + return kIOReturnSuccess; + } + + kern_return_t SubmitSBP2Inquiry(IOUserClientMethodArguments* args) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 2) { + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + const uint8_t allocationLength = static_cast(args->scalarInput[1] & 0xFFu); + return registry_->SubmitInquiry(handle, allocationLength) ? kIOReturnSuccess : kIOReturnError; + } + + kern_return_t GetSBP2InquiryResult(IOUserClientMethodArguments* args) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 1) { + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + auto result = registry_->GetInquiryResult(handle); + if (!result.has_value()) { + return kIOReturnNotFound; + } + + OSData* output = OSData::withBytes(result->data(), static_cast(result->size())); + if (!output) { + return kIOReturnNoMemory; + } + + args->structureOutput = output; + args->structureOutputDescriptor = nullptr; + return kIOReturnSuccess; + } + + kern_return_t SubmitSBP2Command(IOUserClientMethodArguments* args) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 1 || !args->structureInput) { + return kIOReturnBadArgument; + } + + OSData* input = OSDynamicCast(OSData, args->structureInput); + if (!input) { + return kIOReturnBadArgument; + } + + const auto* bytes = static_cast(input->getBytesNoCopy()); + const size_t inputLength = input->getLength(); + if (!bytes || inputLength < sizeof(Wire::SBP2CommandRequestWire)) { + return kIOReturnBadArgument; + } + + const auto* header = reinterpret_cast(bytes); + const size_t expectedLength = + sizeof(Wire::SBP2CommandRequestWire) + + static_cast(header->cdbLength) + + static_cast(header->outgoingLength); + if (inputLength != expectedLength || header->cdbLength == 0) { + return kIOReturnBadArgument; + } + + Protocols::SBP2::SCSI::DataDirection direction{}; + switch (header->direction) { + case 0: + direction = Protocols::SBP2::SCSI::DataDirection::None; + break; + case 1: + direction = Protocols::SBP2::SCSI::DataDirection::FromTarget; + break; + case 2: + direction = Protocols::SBP2::SCSI::DataDirection::ToTarget; + break; + default: + return kIOReturnBadArgument; + } + + Protocols::SBP2::SCSI::CommandRequest request{}; + request.direction = direction; + request.transferLength = header->transferLength; + request.timeoutMs = header->timeoutMs; + request.captureSenseData = header->captureSenseData != 0; + + const uint8_t* cursor = bytes + sizeof(Wire::SBP2CommandRequestWire); + request.cdb.assign(cursor, cursor + header->cdbLength); + cursor += header->cdbLength; + request.outgoingPayload.assign(cursor, cursor + header->outgoingLength); + + const uint64_t handle = args->scalarInput[0]; + return registry_->SubmitCommand(handle, request) ? kIOReturnSuccess : kIOReturnError; + } + + kern_return_t GetSBP2CommandResult(IOUserClientMethodArguments* args) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 1) { + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + auto result = registry_->GetCommandResult(handle); + if (!result.has_value()) { + return kIOReturnNotFound; + } + + Wire::SBP2CommandResultWire header{}; + header.transportStatus = result->transportStatus; + header.sbpStatus = result->sbpStatus; + header.payloadLength = static_cast(result->payload.size()); + header.senseLength = static_cast(result->senseData.size()); + + std::vector serialized( + sizeof(Wire::SBP2CommandResultWire) + + result->payload.size() + + result->senseData.size()); + memcpy(serialized.data(), &header, sizeof(header)); + + size_t offset = sizeof(Wire::SBP2CommandResultWire); + if (!result->payload.empty()) { + memcpy(serialized.data() + offset, result->payload.data(), result->payload.size()); + offset += result->payload.size(); + } + if (!result->senseData.empty()) { + memcpy(serialized.data() + offset, result->senseData.data(), result->senseData.size()); + } + + OSData* output = OSData::withBytes(serialized.data(), static_cast(serialized.size())); + if (!output) { + return kIOReturnNoMemory; + } + + args->structureOutput = output; + args->structureOutputDescriptor = nullptr; + return kIOReturnSuccess; + } + + kern_return_t SubmitSBP2TaskManagement(IOUserClientMethodArguments* args) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 2) { + return kIOReturnBadArgument; + } + + Protocols::SBP2::SBP2ManagementORB::Function function{}; + switch (args->scalarInput[1]) { + case 0x0C: + function = Protocols::SBP2::SBP2ManagementORB::Function::AbortTaskSet; + break; + case 0x0E: + function = Protocols::SBP2::SBP2ManagementORB::Function::LogicalUnitReset; + break; + case 0x0F: + function = Protocols::SBP2::SBP2ManagementORB::Function::TargetReset; + break; + default: + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + return registry_->SubmitTaskManagement(handle, function) ? kIOReturnSuccess : kIOReturnError; + } + + kern_return_t ReleaseSBP2Session(IOUserClientMethodArguments* args, void* owner) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 1) { + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + return registry_->ReleaseSession(handle) ? kIOReturnSuccess : kIOReturnNotFound; } private: ASFW::Protocols::SBP2::AddressSpaceManager* manager_{nullptr}; + ASFW::Protocols::SBP2::SBP2SessionRegistry* registry_{nullptr}; }; } // namespace ASFW::UserClient diff --git a/ASFWDriver/UserClient/WireFormats/SBP2CommandWireFormats.hpp b/ASFWDriver/UserClient/WireFormats/SBP2CommandWireFormats.hpp new file mode 100644 index 00000000..3564e4d3 --- /dev/null +++ b/ASFWDriver/UserClient/WireFormats/SBP2CommandWireFormats.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace ASFW::UserClient::Wire { + +struct __attribute__((packed)) SBP2CommandRequestWire { + uint32_t cdbLength; + uint32_t transferLength; + uint32_t outgoingLength; + uint32_t timeoutMs; + uint8_t direction; + uint8_t captureSenseData; + uint8_t _reserved[2]; + // Followed by: CDB bytes, then outgoing payload bytes. +}; + +struct __attribute__((packed)) SBP2CommandResultWire { + int32_t transportStatus; + uint8_t sbpStatus; + uint8_t _reserved[3]; + uint32_t payloadLength; + uint32_t senseLength; + // Followed by: payload bytes, then sense bytes. +}; + +} // namespace ASFW::UserClient::Wire diff --git a/tests/AddressSpaceManagerTests.cpp b/tests/AddressSpaceManagerTests.cpp index 681a8944..2ba13fe0 100644 --- a/tests/AddressSpaceManagerTests.cpp +++ b/tests/AddressSpaceManagerTests.cpp @@ -261,6 +261,41 @@ TEST(AddressSpaceManagerTests, AutoAllocationReusesFreedGap) { EXPECT_EQ(firstMeta.addressLo, thirdMeta.addressLo); } +TEST(AddressSpaceManagerTests, ClearingRemoteWriteCallbackStopsFurtherNotifications) { + ASFW::Protocols::SBP2::AddressSpaceManager manager(nullptr); + + uint64_t handle = 0; + ASFW::Protocols::SBP2::AddressSpaceManager::AddressRangeMeta meta{}; + ASSERT_EQ(kIOReturnSuccess, + manager.AllocateAddressRange(reinterpret_cast(0xC), + 0xFFFF, + 0x0050'0000, + 8, + &handle, + &meta)); + + int callbackCount = 0; + manager.SetRemoteWriteCallback( + handle, + [&callbackCount](uint64_t, uint32_t, std::span) { + ++callbackCount; + }); + + const std::array payload{0xDE, 0xAD, 0xBE, 0xEF}; + EXPECT_EQ(ASFW::Async::ResponseCode::Complete, + manager.ApplyRemoteWrite( + ComposeAddress(meta.addressHi, meta.addressLo), + std::span(payload.data(), payload.size()))); + EXPECT_EQ(1, callbackCount); + + manager.SetRemoteWriteCallback(handle, {}); + EXPECT_EQ(ASFW::Async::ResponseCode::Complete, + manager.ApplyRemoteWrite( + ComposeAddress(meta.addressHi, meta.addressLo), + std::span(payload.data(), payload.size()))); + EXPECT_EQ(1, callbackCount); +} + TEST(AddressSpaceManagerTests, AutoAllocationRejectsRequestLargerThanWindow) { ASFW::Protocols::SBP2::AddressSpaceManager manager(nullptr); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index aaee4000..b919e4d5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1252,6 +1252,101 @@ target_include_directories(SimITEngineTests PRIVATE ${ASFW_COMMON_INCLUDES}) gtest_discover_tests(SimITEngineTests) +# SBP-2 Address Space Manager Tests +add_executable(AddressSpaceManagerTests + "${ASFW_TESTS_DIR}/AddressSpaceManagerTests.cpp" + "${ASFW_TESTS_DIR}/HardwareInterfaceStub.cpp" +) + +target_link_libraries(AddressSpaceManagerTests + PRIVATE + GTest::gtest_main +) + +target_compile_definitions(AddressSpaceManagerTests + PRIVATE + ASFW_HOST_TEST +) + +target_include_directories(AddressSpaceManagerTests PRIVATE ${ASFW_COMMON_INCLUDES}) + +gtest_discover_tests(AddressSpaceManagerTests) + +# SBP-2 Login Session Tests +add_executable(SBP2LoginSessionTests + "${ASFW_TESTS_DIR}/SBP2LoginSessionTests.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2LoginSession.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2CommandORB.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2ManagementORB.cpp" + "${ASFW_TESTS_DIR}/HardwareInterfaceStub.cpp" + "${ASFW_TESTS_DIR}/LoggingStubs.cpp" +) + +target_link_libraries(SBP2LoginSessionTests + PRIVATE + GTest::gtest_main +) + +target_compile_definitions(SBP2LoginSessionTests + PRIVATE + ASFW_HOST_TEST +) + +target_include_directories(SBP2LoginSessionTests PRIVATE ${ASFW_COMMON_INCLUDES}) + +gtest_discover_tests(SBP2LoginSessionTests) + +# SBP-2 ORB timer tests +add_executable(SBP2ORBTests + "${ASFW_TESTS_DIR}/SBP2ORBTests.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2CommandORB.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2ManagementORB.cpp" + "${ASFW_TESTS_DIR}/HardwareInterfaceStub.cpp" + "${ASFW_TESTS_DIR}/LoggingStubs.cpp" +) + +target_link_libraries(SBP2ORBTests + PRIVATE + GTest::gtest_main +) + +target_compile_definitions(SBP2ORBTests + PRIVATE + ASFW_HOST_TEST +) + +target_include_directories(SBP2ORBTests PRIVATE ${ASFW_COMMON_INCLUDES}) + +gtest_discover_tests(SBP2ORBTests) + +# SBP-2 Session Registry / command tests +add_executable(SBP2SessionRegistryTests + "${ASFW_TESTS_DIR}/SBP2SessionRegistryTests.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2SessionRegistry.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2LoginSession.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2CommandORB.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2ManagementORB.cpp" + "${ASFW_DRIVER_DIR}/Discovery/DeviceManager.cpp" + "${ASFW_DRIVER_DIR}/Discovery/FWDevice.cpp" + "${ASFW_DRIVER_DIR}/Discovery/FWUnit.cpp" + "${ASFW_TESTS_DIR}/HardwareInterfaceStub.cpp" + "${ASFW_TESTS_DIR}/LoggingStubs.cpp" +) + +target_link_libraries(SBP2SessionRegistryTests + PRIVATE + GTest::gtest_main +) + +target_compile_definitions(SBP2SessionRegistryTests + PRIVATE + ASFW_HOST_TEST +) + +target_include_directories(SBP2SessionRegistryTests PRIVATE ${ASFW_COMMON_INCLUDES}) + +gtest_discover_tests(SBP2SessionRegistryTests) + # Device protocol factory routing tests add_executable(DeviceProtocolFactoryTests "${ASFW_TESTS_DIR}/DeviceProtocolFactoryTests.cpp" @@ -1481,44 +1576,3 @@ target_compile_definitions(ROMScanNodeStateMachineTests target_include_directories(ROMScanNodeStateMachineTests PRIVATE ${ASFW_COMMON_INCLUDES}) gtest_discover_tests(ROMScanNodeStateMachineTests) - -add_executable(AddressSpaceManagerTests - "${ASFW_TESTS_DIR}/AddressSpaceManagerTests.cpp" - "${ASFW_TESTS_DIR}/HardwareInterfaceStub.cpp" -) - -target_link_libraries(AddressSpaceManagerTests - PRIVATE - GTest::gtest_main -) - -target_compile_definitions(AddressSpaceManagerTests - PRIVATE - ASFW_HOST_TEST -) - -target_include_directories(AddressSpaceManagerTests PRIVATE ${ASFW_COMMON_INCLUDES}) - -gtest_discover_tests(AddressSpaceManagerTests) - -add_executable(SBP2ORBTests - "${ASFW_TESTS_DIR}/SBP2ORBTests.cpp" - "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2CommandORB.cpp" - "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2ManagementORB.cpp" - "${ASFW_TESTS_DIR}/HardwareInterfaceStub.cpp" - "${ASFW_TESTS_DIR}/LoggingStubs.cpp" -) - -target_link_libraries(SBP2ORBTests - PRIVATE - GTest::gtest_main -) - -target_compile_definitions(SBP2ORBTests - PRIVATE - ASFW_HOST_TEST -) - -target_include_directories(SBP2ORBTests PRIVATE ${ASFW_COMMON_INCLUDES}) - -gtest_discover_tests(SBP2ORBTests) diff --git a/tests/SBP2LoginSessionTests.cpp b/tests/SBP2LoginSessionTests.cpp new file mode 100644 index 00000000..2ed4da11 --- /dev/null +++ b/tests/SBP2LoginSessionTests.cpp @@ -0,0 +1,571 @@ +#include + +#include "ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp" +#include "ASFWDriver/Testing/HostDriverKitStubs.hpp" +#include "tests/mocks/DeferredFireWireBus.hpp" + +#include +#include +#include +#include + +namespace { + +constexpr uint16_t kTargetNodeID = 0x05; + +using ASFW::Protocols::SBP2::AddressSpaceManager; +using ASFW::Protocols::SBP2::LoginState; +using ASFW::Protocols::SBP2::SBP2CommandORB; +using ASFW::Protocols::SBP2::SBP2LoginSession; +using ASFW::Protocols::SBP2::SBP2TargetInfo; +using ASFW::Protocols::SBP2::Wire::FromBE16; +using ASFW::Protocols::SBP2::Wire::CommandBlockAgentOffsets; +using ASFW::Protocols::SBP2::Wire::FromBE32; +using ASFW::Protocols::SBP2::Wire::LoginORB; +using ASFW::Protocols::SBP2::Wire::LoginResponse; +using ASFW::Protocols::SBP2::Wire::NormalizeBusNodeID; +using ASFW::Protocols::SBP2::Wire::ReconnectORB; +using ASFW::Protocols::SBP2::Wire::StatusBlock; +using ASFW::Protocols::SBP2::Wire::ToBE16; +using ASFW::Protocols::SBP2::Wire::ToBE32; +namespace SBPStatus = ASFW::Protocols::SBP2::Wire::SBPStatus; + +uint64_t ComposeAddress(uint16_t hi, uint32_t lo) { + return (static_cast(hi) << 32) | lo; +} + +uint64_t DecodeOrbAddressFromPayload(std::span payload) { + const uint16_t addressHi = + static_cast((static_cast(payload[2]) << 8) | payload[3]); + const uint32_t addressLo = + (static_cast(payload[4]) << 24) | + (static_cast(payload[5]) << 16) | + (static_cast(payload[6]) << 8) | + static_cast(payload[7]); + return ComposeAddress(addressHi, addressLo); +} + +uint32_t DecodeBE32Payload(std::span payload) { + return (static_cast(payload[0]) << 24) | + (static_cast(payload[1]) << 16) | + (static_cast(payload[2]) << 8) | + static_cast(payload[3]); +} + +void ExpectBusyTimeoutWrite(const ASFW::Async::Testing::DeferredFireWireBus::WriteSummary& write, + uint16_t expectedNodeID) { + ASSERT_EQ(4u, write.data.size()); + EXPECT_EQ(0xFFFFu, write.address.addressHi); + EXPECT_EQ(0xF0000210u, write.address.addressLo); + EXPECT_EQ(expectedNodeID, write.address.nodeID); + EXPECT_EQ(static_cast(expectedNodeID & 0x3Fu), write.nodeId.value); + EXPECT_EQ(0x0000000Fu, DecodeBE32Payload(write.data)); +} + +uint32_t ReadQuadlet(AddressSpaceManager& manager, uint64_t address) { + uint32_t value = 0; + EXPECT_EQ(ASFW::Async::ResponseCode::Complete, manager.ReadQuadlet(address, &value)); + return value; +} + +uint64_t ReadORBAddress(AddressSpaceManager& manager, + uint64_t orbAddress, + size_t hiOffset, + size_t loOffset) { + const uint32_t hi = FromBE32(ReadQuadlet(manager, orbAddress + hiOffset)); + const uint32_t lo = FromBE32(ReadQuadlet(manager, orbAddress + loOffset)); + return ComposeAddress(static_cast(hi & 0xFFFFu), lo); +} + +class SessionRig { +public: + SessionRig() + : session(bus, bus, addressManager) { + workQueue.SetManualDispatchForTesting(true); + timeoutQueue.SetManualDispatchForTesting(true); + ASFW::Testing::SetHostMonotonicClockForTesting([this]() { return nowNs; }); + + bus.SetGeneration(ASFW::FW::Generation{1}); + bus.SetLocalNodeID(ASFW::FW::NodeId{0x2A}); + bus.SetDefaultSpeed(ASFW::FW::FwSpeed::S400); + + SBP2TargetInfo info{}; + info.managementAgentOffset = 0x100; + info.lun = 3; + info.managementTimeoutMs = 10; + info.maxORBSize = 32; + info.maxCommandBlockSize = 12; + info.targetNodeId = kTargetNodeID; + + session.SetWorkQueue(&workQueue); + session.SetTimeoutQueue(&timeoutQueue); + session.Configure(info); + } + + ~SessionRig() { + ASFW::Testing::ResetHostMonotonicClockForTesting(); + } + + void DrainReady() { + while (workQueue.DrainReadyForTesting() > 0U || + timeoutQueue.DrainReadyForTesting() > 0U) { + } + } + + void AdvanceMs(uint64_t milliseconds) { + nowNs += milliseconds * 1'000'000ULL; + DrainReady(); + } + + void LoginSuccessfully(uint16_t loginId = 0x0042, + uint32_t commandBlockAgentLo = 0x0020'0000, + bool drainPendingWrites = true) { + ASSERT_TRUE(session.Login()); + ASSERT_EQ(1u, bus.PendingWriteCount()); + + const auto& loginWrite = bus.WriteAt(0); + const uint64_t loginOrbAddress = DecodeOrbAddressFromPayload(loginWrite.data); + const uint64_t loginResponseAddress = + ReadORBAddress(addressManager, + loginOrbAddress, + offsetof(LoginORB, loginResponseAddressHi), + offsetof(LoginORB, loginResponseAddressLo)); + const uint64_t statusAddress = + ReadORBAddress(addressManager, + loginOrbAddress, + offsetof(LoginORB, statusFIFOAddressHi), + offsetof(LoginORB, statusFIFOAddressLo)); + sessionStatusAddress = statusAddress; + + LoginResponse response{}; + response.length = ToBE16(LoginResponse::kSize); + response.loginID = ToBE16(loginId); + response.commandBlockAgentAddressHi = ToBE32(0x0000'FFFFu); + response.commandBlockAgentAddressLo = ToBE32(commandBlockAgentLo); + response.reconnectHold = ToBE16(1); + + ASSERT_TRUE(bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + addressManager.ApplyRemoteWrite( + loginResponseAddress, + std::span{reinterpret_cast(&response), sizeof(response)}); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + addressManager.ApplyRemoteWrite( + statusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + if (drainPendingWrites) { + while (bus.PendingWriteCount() > 0U) { + ASSERT_TRUE(bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + } + } + DrainReady(); + ASSERT_EQ(LoginState::LoggedIn, session.State()); + } + + ASFW::Async::Testing::DeferredFireWireBus bus; + AddressSpaceManager addressManager{nullptr}; + SBP2LoginSession session; + IODispatchQueue workQueue; + IODispatchQueue timeoutQueue; + uint64_t nowNs{0}; + uint64_t sessionStatusAddress{0}; +}; + +TEST(SBP2LoginSessionTests, LoginAckCancelsStaleTimeoutBeforeStatusArrives) { + SessionRig rig; + + ASSERT_TRUE(rig.session.Login()); + ASSERT_EQ(LoginState::LoggingIn, rig.session.State()); + ASSERT_EQ(1u, rig.bus.PendingWriteCount()); + + rig.AdvanceMs(5); + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); + + rig.AdvanceMs(5); + + EXPECT_EQ(LoginState::LoggingIn, rig.session.State()); + EXPECT_EQ(1u, rig.bus.WriteCount()); +} + +TEST(SBP2LoginSessionTests, LoginORBMatchesAppleWireLayout) { + SessionRig rig; + + ASSERT_TRUE(rig.session.Login()); + ASSERT_EQ(1u, rig.bus.PendingWriteCount()); + + const auto& loginWrite = rig.bus.WriteAt(0); + const uint64_t loginOrbAddress = DecodeOrbAddressFromPayload(loginWrite.data); + + const uint32_t quadlet4 = FromBE32( + ReadQuadlet(rig.addressManager, loginOrbAddress + offsetof(LoginORB, options))); + const uint32_t quadlet5 = FromBE32( + ReadQuadlet(rig.addressManager, loginOrbAddress + offsetof(LoginORB, passwordLength))); + + EXPECT_EQ(0x9000u, static_cast(quadlet4 >> 16)); + EXPECT_EQ(3u, static_cast(quadlet4 & 0xFFFFu)); + EXPECT_EQ(0u, static_cast(quadlet5 >> 16)); + EXPECT_EQ(LoginResponse::kSize, static_cast(quadlet5 & 0xFFFFu)); +} + +TEST(SBP2LoginSessionTests, LoginORBUsesFullBusNodeIdInEmbeddedAddresses) { + SessionRig rig; + + ASSERT_TRUE(rig.session.Login()); + ASSERT_EQ(1u, rig.bus.PendingWriteCount()); + + const auto& loginWrite = rig.bus.WriteAt(0); + ASSERT_EQ(8u, loginWrite.data.size()); + + const uint16_t payloadNode = + static_cast((static_cast(loginWrite.data[0]) << 8) | + loginWrite.data[1]); + const uint64_t loginOrbAddress = DecodeOrbAddressFromPayload(loginWrite.data); + const uint32_t responseHi = FromBE32( + ReadQuadlet(rig.addressManager, loginOrbAddress + offsetof(LoginORB, loginResponseAddressHi))); + const uint32_t statusHi = FromBE32( + ReadQuadlet(rig.addressManager, loginOrbAddress + offsetof(LoginORB, statusFIFOAddressHi))); + + const uint16_t expectedNode = NormalizeBusNodeID(0x2A); + EXPECT_EQ(expectedNode, payloadNode); + EXPECT_EQ((static_cast(expectedNode) << 16) | 0xFFFFu, responseHi); + EXPECT_EQ((static_cast(expectedNode) << 16) | 0xFFFFu, statusHi); +} + +TEST(SBP2LoginSessionTests, QueuedUnsolicitedStatusEnableUsesDerivedAddressAfterLogin) { + SessionRig rig; + + rig.session.EnableUnsolicitedStatus(); + constexpr uint32_t commandBlockAgentLo = 0x0030'0000; + rig.LoginSuccessfully(0x0042, commandBlockAgentLo); + + ASSERT_GE(rig.bus.WriteCount(), 3u); + const auto& unsolicitedWrite = rig.bus.WriteAt(1); + EXPECT_EQ(0xFFFFu, unsolicitedWrite.address.addressHi); + EXPECT_EQ(commandBlockAgentLo + CommandBlockAgentOffsets::kUnsolicitedStatusEnable, + unsolicitedWrite.address.addressLo); +} + +TEST(SBP2LoginSessionTests, LoginWritesBusyTimeoutRegister) { + SessionRig rig; + + rig.LoginSuccessfully(); + + ASSERT_GE(rig.bus.WriteCount(), 2u); + ExpectBusyTimeoutWrite(rig.bus.WriteAt(1), kTargetNodeID); +} + +TEST(SBP2LoginSessionTests, ReconnectReplaysBusyTimeoutRegister) { + SessionRig rig; + rig.LoginSuccessfully(); + + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + ASSERT_EQ(LoginState::Suspended, rig.session.State()); + ASSERT_TRUE(rig.session.Reconnect()); + ASSERT_EQ(LoginState::Reconnecting, rig.session.State()); + + const size_t reconnectWriteIndex = rig.bus.WriteCount() - 1; + const uint64_t reconnectOrbAddress = + DecodeOrbAddressFromPayload(rig.bus.WriteAt(reconnectWriteIndex).data); + const uint64_t reconnectStatusAddress = + ReadORBAddress(rig.addressManager, + reconnectOrbAddress, + offsetof(ReconnectORB, statusFIFOAddressHi), + offsetof(ReconnectORB, statusFIFOAddressLo)); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + rig.addressManager.ApplyRemoteWrite( + reconnectStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + ASSERT_GE(rig.bus.WriteCount(), reconnectWriteIndex + 2); + ExpectBusyTimeoutWrite(rig.bus.WriteAt(reconnectWriteIndex + 1), kTargetNodeID); +} + +TEST(SBP2LoginSessionTests, BusyTimeoutReplayCancelsInFlightWrite) { + SessionRig rig; + + rig.LoginSuccessfully(0x0042, 0x0020'0000, false); + const auto firstBusyHandle = rig.bus.WriteAt(1).handle; + + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + ASSERT_TRUE(rig.session.Reconnect()); + + const size_t reconnectWriteIndex = rig.bus.WriteCount() - 1; + const uint64_t reconnectOrbAddress = + DecodeOrbAddressFromPayload(rig.bus.WriteAt(reconnectWriteIndex).data); + const uint64_t reconnectStatusAddress = + ReadORBAddress(rig.addressManager, + reconnectOrbAddress, + offsetof(ReconnectORB, statusFIFOAddressHi), + offsetof(ReconnectORB, statusFIFOAddressLo)); + + ASSERT_TRUE(rig.bus.CompleteWrite(rig.bus.WriteAt(reconnectWriteIndex).handle, + ASFW::Async::AsyncStatus::kSuccess)); + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + rig.addressManager.ApplyRemoteWrite( + reconnectStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + EXPECT_FALSE(rig.bus.CompleteWrite(firstBusyHandle, ASFW::Async::AsyncStatus::kSuccess)); + ASSERT_GE(rig.bus.WriteCount(), reconnectWriteIndex + 2); + ExpectBusyTimeoutWrite(rig.bus.WriteAt(reconnectWriteIndex + 1), kTargetNodeID); +} + +TEST(SBP2LoginSessionTests, BusResetWhileLoggingInRetriesLoginAfterDelay) { + SessionRig rig; + + ASSERT_TRUE(rig.session.Login()); + ASSERT_EQ(LoginState::LoggingIn, rig.session.State()); + ASSERT_EQ(1u, rig.bus.WriteCount()); + + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + EXPECT_EQ(LoginState::Idle, rig.session.State()); + + rig.AdvanceMs(99); + EXPECT_EQ(1u, rig.bus.WriteCount()); + + rig.AdvanceMs(1); + EXPECT_EQ(LoginState::LoggingIn, rig.session.State()); + EXPECT_EQ(2u, rig.bus.WriteCount()); + EXPECT_EQ(2u, rig.session.Generation()); +} + +TEST(SBP2LoginSessionTests, LoginRetryDelayUsesTimeoutQueueInsteadOfWorkQueue) { + SessionRig rig; + + ASSERT_TRUE(rig.session.Login()); + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + + EXPECT_EQ(0u, rig.workQueue.PendingTaskCountForTesting()); + EXPECT_GT(rig.timeoutQueue.PendingTaskCountForTesting(), 0u); + + int workExecuted = 0; + rig.workQueue.DispatchAsync([&workExecuted]() { ++workExecuted; }); + rig.DrainReady(); + + EXPECT_EQ(1, workExecuted); +} + +TEST(SBP2LoginSessionTests, BusResetWhileReconnectingRetriesReconnectAfterDelay) { + SessionRig rig; + rig.LoginSuccessfully(); + + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + ASSERT_EQ(LoginState::Suspended, rig.session.State()); + ASSERT_TRUE(rig.session.Reconnect()); + ASSERT_EQ(LoginState::Reconnecting, rig.session.State()); + ASSERT_EQ(2u, rig.session.Generation()); + ASSERT_EQ(3u, rig.bus.WriteCount()); + + rig.bus.SetGeneration(ASFW::FW::Generation{3}); + rig.session.HandleBusReset(3); + EXPECT_EQ(LoginState::Suspended, rig.session.State()); + + rig.AdvanceMs(99); + EXPECT_EQ(3u, rig.bus.WriteCount()); + + rig.AdvanceMs(1); + EXPECT_EQ(LoginState::Reconnecting, rig.session.State()); + EXPECT_EQ(4u, rig.bus.WriteCount()); + EXPECT_EQ(3u, rig.session.Generation()); +} + +TEST(SBP2LoginSessionTests, ImmediateORBRetryStaysBoundToOriginalORBAndQueuesNextImmediate) { + SessionRig rig; + rig.LoginSuccessfully(); + + SBP2CommandORB first(rig.addressManager, &rig.session, 16); + first.SetFlags(SBP2CommandORB::kImmediate); + first.SetTimeout(50); + + SBP2CommandORB second(rig.addressManager, &rig.session, 16); + second.SetFlags(SBP2CommandORB::kImmediate); + + ASSERT_TRUE(rig.session.SubmitORB(&first)); + ASSERT_EQ(1u, rig.bus.PendingWriteCount()); + const size_t firstFetchWriteIndex = rig.bus.WriteCount() - 1; + const uint32_t firstAddressLo = static_cast( + DecodeOrbAddressFromPayload(rig.bus.WriteAt(firstFetchWriteIndex).data)); + + ASSERT_TRUE(rig.session.SubmitORB(&second)); + EXPECT_EQ(3u, rig.bus.WriteCount()); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kTimeout)); + rig.DrainReady(); + + rig.AdvanceMs(999); + EXPECT_EQ(3u, rig.bus.WriteCount()); + + rig.AdvanceMs(1); + ASSERT_EQ(4u, rig.bus.WriteCount()); + const uint32_t retryAddressLo = static_cast( + DecodeOrbAddressFromPayload(rig.bus.WriteAt(3).data)); + EXPECT_EQ(firstAddressLo, retryAddressLo); +} + +TEST(SBP2LoginSessionTests, SolicitedStatusCompletesORBMatchingByORBAddress) { + SessionRig rig; + rig.LoginSuccessfully(); + + ASSERT_NE(0u, rig.session.CommandBlockAgent().addressLo); + + SBP2CommandORB first(rig.addressManager, &rig.session, 16); + first.SetFlags(0); + int firstStatus = 99; + first.SetCompletionCallback([&firstStatus](int status, uint8_t) { firstStatus = status; }); + + SBP2CommandORB second(rig.addressManager, &rig.session, 16); + second.SetFlags(0); + int secondStatus = 99; + second.SetCompletionCallback([&secondStatus](int status, uint8_t) { secondStatus = status; }); + + ASSERT_TRUE(rig.session.SubmitORB(&first)); + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); + + ASSERT_TRUE(rig.session.SubmitORB(&second)); + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); + + StatusBlock status{}; + const auto firstAddress = first.GetORBAddress(); + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + status.orbOffsetHi = ToBE16(firstAddress.addressHi); + status.orbOffsetLo = ToBE32(firstAddress.addressLo); + + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + EXPECT_EQ(0, firstStatus); + EXPECT_EQ(99, secondStatus); +} + +TEST(SBP2LoginSessionTests, ChainedORBLinkFailureReturnsFalseWithoutDoorbellWrite) { + SessionRig rig; + rig.LoginSuccessfully(); + + auto* firstOwner = reinterpret_cast(0x101); + auto* secondOwner = reinterpret_cast(0x102); + + SBP2CommandORB first(rig.addressManager, firstOwner, 16); + first.SetFlags(0); + ASSERT_TRUE(rig.session.SubmitORB(&first)); + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); + + rig.addressManager.ReleaseOwner(firstOwner); + + SBP2CommandORB second(rig.addressManager, secondOwner, 16); + second.SetFlags(0); + + int secondTransportStatus = 99; + second.SetCompletionCallback([&secondTransportStatus](int status, uint8_t) { + secondTransportStatus = status; + }); + + const size_t writeCountBeforeSubmit = rig.bus.WriteCount(); + EXPECT_FALSE(rig.session.SubmitORB(&second)); + EXPECT_EQ(writeCountBeforeSubmit, rig.bus.WriteCount()); + EXPECT_EQ(-1, secondTransportStatus); + EXPECT_FALSE(second.IsAppended()); +} + +TEST(SBP2LoginSessionTests, ImmediateORBWriteRetryExhaustionFailsActiveCommandAndResetsFetchAgent) { + SessionRig rig; + rig.LoginSuccessfully(); + + SBP2CommandORB orb(rig.addressManager, &rig.session, 16); + orb.SetFlags(SBP2CommandORB::kImmediate); + + int callbackStatus = 99; + int callbackCount = 0; + orb.SetCompletionCallback([&](int status, uint8_t) { + ++callbackCount; + callbackStatus = status; + }); + + const auto commandBlock = rig.session.CommandBlockAgent(); + const size_t writesBeforeSubmit = rig.bus.WriteCount(); + ASSERT_TRUE(rig.session.SubmitORB(&orb)); + orb.SetFetchAgentWriteRetries(0); + ASSERT_EQ(writesBeforeSubmit + 1, rig.bus.WriteCount()); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kTimeout)); + rig.DrainReady(); + + ASSERT_EQ(1, callbackCount); + EXPECT_EQ(-1, callbackStatus); + EXPECT_FALSE(orb.IsAppended()); + EXPECT_GE(rig.bus.WriteCount(), writesBeforeSubmit + 2); + EXPECT_EQ(commandBlock.addressLo + CommandBlockAgentOffsets::kAgentReset, + rig.bus.WriteAt(rig.bus.WriteCount() - 1).address.addressLo); + + EXPECT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); +} + +TEST( + SBP2LoginSessionTests, + ImmediateORBWriteRetryExhaustionClearsPendingImmediateQueueAndFailsActiveCommandOnce) { + SessionRig rig; + rig.LoginSuccessfully(); + + SBP2CommandORB first(rig.addressManager, &rig.session, 16); + first.SetFlags(SBP2CommandORB::kImmediate); + int firstCallbackStatus = 99; + int firstCallbackCount = 0; + first.SetCompletionCallback([&](int status, uint8_t) { + ++firstCallbackCount; + firstCallbackStatus = status; + }); + + SBP2CommandORB second(rig.addressManager, &rig.session, 16); + second.SetFlags(SBP2CommandORB::kImmediate); + int secondCallbackStatus = 99; + int secondCallbackCount = 0; + second.SetCompletionCallback([&](int status, uint8_t) { + ++secondCallbackCount; + secondCallbackStatus = status; + }); + + const auto commandBlock = rig.session.CommandBlockAgent(); + const size_t writesBeforeSubmit = rig.bus.WriteCount(); + + ASSERT_TRUE(rig.session.SubmitORB(&first)); + ASSERT_TRUE(rig.session.SubmitORB(&second)); + first.SetFetchAgentWriteRetries(0); + EXPECT_EQ(writesBeforeSubmit + 1, rig.bus.WriteCount()); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kTimeout)); + rig.DrainReady(); + + EXPECT_EQ(1, firstCallbackCount); + EXPECT_EQ(-1, firstCallbackStatus); + EXPECT_EQ(1, secondCallbackCount); + EXPECT_EQ(-1, secondCallbackStatus); + EXPECT_FALSE(first.IsAppended()); + EXPECT_FALSE(second.IsAppended()); + EXPECT_GE(rig.bus.WriteCount(), writesBeforeSubmit + 2); + EXPECT_EQ(commandBlock.addressLo + CommandBlockAgentOffsets::kAgentReset, + rig.bus.WriteAt(rig.bus.WriteCount() - 1).address.addressLo); + EXPECT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); +} + +} // namespace diff --git a/tests/SBP2ORBTests.cpp b/tests/SBP2ORBTests.cpp index 689494bd..524aabae 100644 --- a/tests/SBP2ORBTests.cpp +++ b/tests/SBP2ORBTests.cpp @@ -2,7 +2,6 @@ #include "ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp" #include "ASFWDriver/Protocols/SBP2/SBP2ManagementORB.hpp" -#include "ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp" #include "ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp" #include "ASFWDriver/Testing/HostDriverKitStubs.hpp" #include "tests/mocks/DeferredFireWireBus.hpp" @@ -17,8 +16,6 @@ namespace { using ASFW::Protocols::SBP2::AddressSpaceManager; using ASFW::Protocols::SBP2::SBP2CommandORB; using ASFW::Protocols::SBP2::SBP2ManagementORB; -using ASFW::Protocols::SBP2::SBP2PageTable; -using ASFW::Protocols::SBP2::Wire::FromBE16; using ASFW::Protocols::SBP2::Wire::FromBE32; using ASFW::Protocols::SBP2::Wire::ManagementAgentAddressLo; using ASFW::Protocols::SBP2::Wire::NormalizeBusNodeID; @@ -61,7 +58,8 @@ uint64_t ReadStatusAddressFromManagementORB(AddressSpaceManager& manager, uint64 class ORBTimerRig { public: ORBTimerRig() { - queue.SetManualDispatchForTesting(true); + workQueue.SetManualDispatchForTesting(true); + timeoutQueue.SetManualDispatchForTesting(true); ASFW::Testing::SetHostMonotonicClockForTesting([this]() { return nowNs; }); bus.SetGeneration(ASFW::FW::Generation{1}); @@ -74,7 +72,8 @@ class ORBTimerRig { } void DrainReady() { - while (queue.DrainReadyForTesting() > 0U) { + while (workQueue.DrainReadyForTesting() > 0U || + timeoutQueue.DrainReadyForTesting() > 0U) { } } @@ -85,7 +84,8 @@ class ORBTimerRig { ASFW::Async::Testing::DeferredFireWireBus bus; AddressSpaceManager addressManager{nullptr}; - IODispatchQueue queue; + IODispatchQueue workQueue; + IODispatchQueue timeoutQueue; uint64_t nowNs{0}; }; @@ -97,7 +97,7 @@ TEST(SBP2ORBTests, CommandORBTimerFiresOnHostQueue) { orb.SetTimeout(5); orb.SetCompletionCallback([&completionStatus](int status, uint8_t) { completionStatus = status; }); - orb.StartTimer(&rig.queue); + orb.StartTimer(&rig.workQueue, &rig.timeoutQueue); rig.AdvanceMs(5); EXPECT_EQ(-1, completionStatus); @@ -111,7 +111,7 @@ TEST(SBP2ORBTests, CommandORBCancelSuppressesPendingTimeout) { orb.SetTimeout(5); orb.SetCompletionCallback([&completionCount](int, uint8_t) { ++completionCount; }); - orb.StartTimer(&rig.queue); + orb.StartTimer(&rig.workQueue, &rig.timeoutQueue); orb.CancelTimer(); rig.AdvanceMs(5); @@ -127,65 +127,13 @@ TEST(SBP2ORBTests, CommandORBDestructionInvalidatesPendingTimeout) { rig.addressManager, reinterpret_cast(0x3), 16); orb->SetTimeout(5); orb->SetCompletionCallback([&completionCount](int, uint8_t) { ++completionCount; }); - orb->StartTimer(&rig.queue); + orb->StartTimer(&rig.workQueue, &rig.timeoutQueue); } rig.AdvanceMs(5); EXPECT_EQ(0, completionCount); } -TEST(SBP2ORBTests, PageTableUsesDirectDescriptorForSingleAlignedSegment) { - ORBTimerRig rig; - - SBP2PageTable pageTable(rig.addressManager, reinterpret_cast(0x40)); - const std::array segments{{ - {.address = 0x0001'2345'6000ULL, .length = 512}, - }}; - - ASSERT_TRUE(pageTable.Build(segments, 0x21)); - - const auto& result = pageTable.GetResult(); - EXPECT_TRUE(result.isDirect); - EXPECT_EQ(1u, pageTable.EntryCount()); - EXPECT_EQ(0x0001u, FromBE32(result.dataDescriptorHi) & 0xFFFFu); - EXPECT_EQ(0x2345'6000u, FromBE32(result.dataDescriptorLo)); - EXPECT_EQ(512u, FromBE16(result.dataSize)); - EXPECT_EQ(0u, result.options); -} - -TEST(SBP2ORBTests, PageTableSplitsSegmentsIntoPublishedEntries) { - ORBTimerRig rig; - - SBP2PageTable pageTable(rig.addressManager, reinterpret_cast(0x41)); - const std::array segments{{ - {.address = 0x0001'0000'1000ULL, .length = 0x30}, - }}; - - ASSERT_TRUE(pageTable.Build(segments, 0x21, 0x10)); - - const auto& result = pageTable.GetResult(); - ASSERT_FALSE(result.isDirect); - ASSERT_EQ(3u, pageTable.EntryCount()); - EXPECT_EQ(3u, FromBE16(result.dataSize)); - EXPECT_EQ(ASFW::Protocols::SBP2::Wire::Options::kPageTableUnrestricted, - result.options); - - const uint32_t descriptorHi = FromBE32(result.dataDescriptorHi); - const uint16_t expectedNode = NormalizeBusNodeID(0x21); - EXPECT_EQ(expectedNode, static_cast(descriptorHi >> 16)); - EXPECT_EQ(0xFFFFu, descriptorHi & 0xFFFFu); - - const uint64_t tableAddress = - ComposeAddress(static_cast(descriptorHi & 0xFFFFu), - FromBE32(result.dataDescriptorLo)); - const uint32_t firstEntryHeader = FromBE32(ReadQuadlet(rig.addressManager, tableAddress)); - const uint32_t firstEntryLo = FromBE32(ReadQuadlet(rig.addressManager, tableAddress + 4)); - - EXPECT_EQ(0x0010u, firstEntryHeader >> 16); - EXPECT_EQ(0x0001u, firstEntryHeader & 0xFFFFu); - EXPECT_EQ(0x0000'1000u, firstEntryLo); -} - TEST(SBP2ORBTests, ManagementORBStatusWriteCancelsTimeout) { ORBTimerRig rig; @@ -195,7 +143,8 @@ TEST(SBP2ORBTests, ManagementORBStatusWriteCancelsTimeout) { orb.SetManagementAgentOffset(0x80); orb.SetTargetNode(1, 0x3F); orb.SetTimeout(5); - orb.SetWorkQueue(&rig.queue); + orb.SetWorkQueue(&rig.workQueue); + orb.SetTimeoutQueue(&rig.timeoutQueue); int completionStatus = 99; orb.SetCompletionCallback([&completionStatus](int status) { completionStatus = status; }); @@ -213,6 +162,8 @@ TEST(SBP2ORBTests, ManagementORBStatusWriteCancelsTimeout) { StatusBlock status{}; status.details = 0; status.sbpStatus = SBPStatus::kNoAdditionalInfo; + status.orbOffsetHi = ToBE16(static_cast((orbAddress >> 32) & 0xFFFFu)); + status.orbOffsetLo = ToBE32(static_cast(orbAddress & 0xFFFF'FFFFu)); rig.addressManager.ApplyRemoteWrite( ReadStatusAddressFromManagementORB(rig.addressManager, orbAddress), std::span{reinterpret_cast(&status), sizeof(status)}); @@ -257,7 +208,7 @@ TEST(SBP2ORBTests, CommandORBDirectDescriptorUsesFullBusNodeId) { descriptor.isDirect = true; orb.SetDataDescriptor(descriptor); - orb.PrepareForExecution(0x21, ASFW::FW::FwSpeed::S400, 6); + ASSERT_EQ(kIOReturnSuccess, orb.PrepareForExecution(0x21, ASFW::FW::FwSpeed::S400, 6)); const auto orbAddress = orb.GetORBAddress(); const uint64_t packedAddress = ComposeAddress(orbAddress.addressHi, orbAddress.addressLo); @@ -281,7 +232,8 @@ TEST(SBP2ORBTests, ManagementORBDestructionInvalidatesPendingTimeout) { orb->SetManagementAgentOffset(0x81); orb->SetTargetNode(1, 0x3F); orb->SetTimeout(5); - orb->SetWorkQueue(&rig.queue); + orb->SetWorkQueue(&rig.workQueue); + orb->SetTimeoutQueue(&rig.timeoutQueue); orb->SetCompletionCallback([&completionCount](int) { ++completionCount; }); ASSERT_TRUE(orb->Execute()); @@ -293,4 +245,140 @@ TEST(SBP2ORBTests, ManagementORBDestructionInvalidatesPendingTimeout) { EXPECT_EQ(0, completionCount); } +TEST(SBP2ORBTests, ManagementORBPropagatesDeviceStatusFailure) { + ORBTimerRig rig; + + SBP2ManagementORB orb(rig.bus, rig.bus, rig.addressManager, reinterpret_cast(0x8)); + orb.SetFunction(SBP2ManagementORB::Function::LogicalUnitReset); + orb.SetLoginID(0x44); + orb.SetManagementAgentOffset(0x90); + orb.SetTargetNode(1, 0x3F); + orb.SetTimeout(5); + orb.SetWorkQueue(&rig.workQueue); + orb.SetTimeoutQueue(&rig.timeoutQueue); + + int completionStatus = 99; + orb.SetCompletionCallback([&completionStatus](int status) { completionStatus = status; }); + + ASSERT_TRUE(orb.Execute()); + const auto& write = rig.bus.WriteAt(0); + const uint64_t orbAddress = DecodeOrbAddressFromPayload(write.data); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kFunctionRejected; + status.orbOffsetHi = ToBE16(static_cast((orbAddress >> 32) & 0xFFFFu)); + status.orbOffsetLo = ToBE32(static_cast(orbAddress & 0xFFFF'FFFFu)); + rig.addressManager.ApplyRemoteWrite( + ReadStatusAddressFromManagementORB(rig.addressManager, orbAddress), + std::span{reinterpret_cast(&status), sizeof(status)}); + + rig.DrainReady(); + EXPECT_EQ(-4, completionStatus); +} + +TEST(SBP2ORBTests, ManagementORBRejectsMalformedStatusPayload) { + ORBTimerRig rig; + + SBP2ManagementORB orb(rig.bus, rig.bus, rig.addressManager, reinterpret_cast(0x9)); + orb.SetFunction(SBP2ManagementORB::Function::AbortTaskSet); + orb.SetLoginID(0x45); + orb.SetManagementAgentOffset(0x91); + orb.SetTargetNode(1, 0x3F); + orb.SetTimeout(5); + orb.SetWorkQueue(&rig.workQueue); + orb.SetTimeoutQueue(&rig.timeoutQueue); + + int completionStatus = 99; + orb.SetCompletionCallback([&completionStatus](int status) { completionStatus = status; }); + + ASSERT_TRUE(orb.Execute()); + const auto& write = rig.bus.WriteAt(0); + const uint64_t orbAddress = DecodeOrbAddressFromPayload(write.data); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); + + const std::array shortPayload{0, 0, 0, 0}; + rig.addressManager.ApplyRemoteWrite( + ReadStatusAddressFromManagementORB(rig.addressManager, orbAddress), + std::span{shortPayload.data(), shortPayload.size()}); + + rig.DrainReady(); + EXPECT_EQ(-3, completionStatus); +} + +TEST(SBP2ORBTests, ManagementORBRejectsMismatchedStatusORBAddress) { + ORBTimerRig rig; + + SBP2ManagementORB orb(rig.bus, rig.bus, rig.addressManager, reinterpret_cast(0xA)); + orb.SetFunction(SBP2ManagementORB::Function::AbortTaskSet); + orb.SetLoginID(0x46); + orb.SetManagementAgentOffset(0x92); + orb.SetTargetNode(1, 0x3F); + orb.SetTimeout(5); + orb.SetWorkQueue(&rig.workQueue); + orb.SetTimeoutQueue(&rig.timeoutQueue); + + int completionStatus = 99; + orb.SetCompletionCallback([&completionStatus](int status) { completionStatus = status; }); + + ASSERT_TRUE(orb.Execute()); + const auto& write = rig.bus.WriteAt(0); + const uint64_t orbAddress = DecodeOrbAddressFromPayload(write.data); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + status.orbOffsetHi = ToBE16(static_cast(((orbAddress + 8) >> 32) & 0xFFFFu)); + status.orbOffsetLo = ToBE32(static_cast((orbAddress + 8) & 0xFFFF'FFFFu)); + rig.addressManager.ApplyRemoteWrite( + ReadStatusAddressFromManagementORB(rig.addressManager, orbAddress), + std::span{reinterpret_cast(&status), sizeof(status)}); + + rig.DrainReady(); + EXPECT_EQ(-3, completionStatus); +} + +TEST(SBP2ORBTests, ManagementORBDestroyedAfterExecuteIgnoresPendingWriteAndStatus) { + ORBTimerRig rig; + + int completionCount = 0; + uint64_t statusAddress = 0; + { + auto orb = std::make_unique( + rig.bus, rig.bus, rig.addressManager, reinterpret_cast(0xB)); + orb->SetFunction(SBP2ManagementORB::Function::AbortTaskSet); + orb->SetLoginID(0x47); + orb->SetManagementAgentOffset(0x93); + orb->SetTargetNode(1, 0x3F); + orb->SetTimeout(5); + orb->SetWorkQueue(&rig.workQueue); + orb->SetTimeoutQueue(&rig.timeoutQueue); + orb->SetCompletionCallback([&completionCount](int) { ++completionCount; }); + + ASSERT_TRUE(orb->Execute()); + const auto& write = rig.bus.WriteAt(0); + const uint64_t orbAddress = DecodeOrbAddressFromPayload(write.data); + statusAddress = ReadStatusAddressFromManagementORB(rig.addressManager, orbAddress); + } + + EXPECT_EQ(0u, rig.bus.PendingWriteCount()); + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + rig.addressManager.ApplyRemoteWrite( + statusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + rig.AdvanceMs(5); + + EXPECT_EQ(0, completionCount); +} + } // namespace diff --git a/tests/SBP2SessionRegistryTests.cpp b/tests/SBP2SessionRegistryTests.cpp new file mode 100644 index 00000000..2f9df3de --- /dev/null +++ b/tests/SBP2SessionRegistryTests.cpp @@ -0,0 +1,558 @@ +#include + +#include "ASFWDriver/Discovery/DeviceManager.hpp" +#include "ASFWDriver/Protocols/SBP2/SBP2ManagementORB.hpp" +#include "ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp" +#include "ASFWDriver/Protocols/SBP2/SCSICommandSet.hpp" +#include "ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp" +#include "ASFWDriver/Testing/HostDriverKitStubs.hpp" +#include "tests/mocks/DeferredFireWireBus.hpp" + +#include +#include +#include + +namespace { + +constexpr uint32_t kSBP2UnitSpecId = 0x00609E; +constexpr uint32_t kSBP2UnitSwVersion = 0x010483; + +using ASFW::Discovery::CfgKey; +using ASFW::Discovery::ConfigROM; +using ASFW::Discovery::DeviceKind; +using ASFW::Discovery::DeviceManager; +using ASFW::Discovery::DeviceRecord; +using ASFW::Discovery::Generation; +using ASFW::Discovery::LifeState; +using ASFW::Discovery::LinkPolicy; +using ASFW::Discovery::RomEntry; +using ASFW::Protocols::SBP2::AddressSpaceManager; +using ASFW::Protocols::SBP2::SBP2ManagementORB; +using ASFW::Protocols::SBP2::SBP2SessionRegistry; +namespace SCSI = ASFW::Protocols::SBP2::SCSI; +using ASFW::Protocols::SBP2::Wire::FromBE16; +using ASFW::Protocols::SBP2::Wire::FromBE32; +using ASFW::Protocols::SBP2::Wire::LoginORB; +using ASFW::Protocols::SBP2::Wire::LoginResponse; +using ASFW::Protocols::SBP2::Wire::NormalORB; +using ASFW::Protocols::SBP2::Wire::ReconnectORB; +using ASFW::Protocols::SBP2::Wire::StatusBlock; +using ASFW::Protocols::SBP2::Wire::TaskManagementORB; +using ASFW::Protocols::SBP2::Wire::ToBE16; +using ASFW::Protocols::SBP2::Wire::ToBE32; +namespace SBPStatus = ASFW::Protocols::SBP2::Wire::SBPStatus; + +uint64_t ComposeAddress(uint16_t hi, uint32_t lo) { + return (static_cast(hi) << 32) | lo; +} + +uint64_t DecodeAddressFromWritePayload(std::span payload) { + const uint16_t addressHi = + static_cast((static_cast(payload[2]) << 8) | payload[3]); + const uint32_t addressLo = + (static_cast(payload[4]) << 24) | + (static_cast(payload[5]) << 16) | + (static_cast(payload[6]) << 8) | + static_cast(payload[7]); + return ComposeAddress(addressHi, addressLo); +} + +uint32_t ReadQuadlet(AddressSpaceManager& manager, uint64_t address) { + uint32_t value = 0; + EXPECT_EQ(ASFW::Async::ResponseCode::Complete, manager.ReadQuadlet(address, &value)); + return value; +} + +uint64_t ReadORBAddress(AddressSpaceManager& manager, + uint64_t orbAddress, + size_t hiOffset, + size_t loOffset) { + const uint32_t hi = FromBE32(ReadQuadlet(manager, orbAddress + hiOffset)); + const uint32_t lo = FromBE32(ReadQuadlet(manager, orbAddress + loOffset)); + return ComposeAddress(static_cast(hi & 0xFFFFu), lo); +} + +uint64_t ReadDataBufferAddress(AddressSpaceManager& manager, uint64_t orbAddress) { + const uint32_t hi = FromBE32( + ReadQuadlet(manager, orbAddress + offsetof(NormalORB, dataDescriptorHi))); + const uint32_t lo = FromBE32( + ReadQuadlet(manager, orbAddress + offsetof(NormalORB, dataDescriptorLo))); + return ComposeAddress(static_cast(hi & 0xFFFFu), lo); +} + +uint16_t ReadTaskManagementFunction(AddressSpaceManager& manager, uint64_t orbAddress) { + const uint32_t optionsAndLoginID = + FromBE32(ReadQuadlet(manager, orbAddress + offsetof(TaskManagementORB, options))); + return static_cast((optionsAndLoginID >> 16) & 0x000Fu); +} + +uint16_t ReadTaskManagementLoginID(AddressSpaceManager& manager, uint64_t orbAddress) { + const uint32_t optionsAndLoginID = + FromBE32(ReadQuadlet(manager, orbAddress + offsetof(TaskManagementORB, options))); + return static_cast(optionsAndLoginID & 0xFFFFu); +} + +uint64_t ReadTaskManagementStatusAddress(AddressSpaceManager& manager, uint64_t orbAddress) { + return ReadORBAddress(manager, + orbAddress, + offsetof(TaskManagementORB, statusFIFOAddressHi), + offsetof(TaskManagementORB, statusFIFOAddressLo)); +} + +void CompleteTaskManagementStatus(AddressSpaceManager& manager, + uint64_t statusAddress, + uint64_t orbAddress, + uint8_t sbpStatus = SBPStatus::kNoAdditionalInfo) { + StatusBlock status{}; + status.details = 0; + status.sbpStatus = sbpStatus; + status.orbOffsetHi = ToBE16(static_cast((orbAddress >> 32) & 0xFFFFu)); + status.orbOffsetLo = ToBE32(static_cast(orbAddress & 0xFFFF'FFFFu)); + manager.ApplyRemoteWrite( + statusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); +} + +class SessionRegistryRig { +public: + SessionRegistryRig() + : registry(bus, bus, addressManager, deviceManager, &queue) { + queue.SetManualDispatchForTesting(true); + ASFW::Testing::SetHostMonotonicClockForTesting([this]() { return nowNs; }); + + bus.SetGeneration(ASFW::FW::Generation{1}); + bus.SetLocalNodeID(ASFW::FW::NodeId{0x2A}); + bus.SetDefaultSpeed(ASFW::FW::FwSpeed::S400); + + UpsertDevice(Generation{1}, 0x32); + } + + ~SessionRegistryRig() { + ASFW::Testing::ResetHostMonotonicClockForTesting(); + } + + void UpsertDevice(Generation generation, uint8_t nodeId) { + DeviceRecord record{}; + record.guid = kGuid; + record.vendorId = 0x001122; + record.modelId = 0x334455; + record.kind = DeviceKind::Unknown; + record.vendorName = "Scanner Vendor"; + record.modelName = "Scanner Model"; + record.gen = generation; + record.nodeId = nodeId; + record.link = LinkPolicy{}; + record.state = LifeState::Ready; + + ConfigROM rom{}; + rom.gen = generation; + rom.nodeId = record.nodeId; + rom.vendorName = record.vendorName; + rom.modelName = record.modelName; + rom.rootDirMinimal = { + RomEntry{CfgKey::Unit_Spec_Id, kSBP2UnitSpecId, 0, 0}, + RomEntry{CfgKey::Unit_Sw_Version, kSBP2UnitSwVersion, 0, 0}, + RomEntry{CfgKey::Logical_Unit_Number, 0x000002, 0, 0}, + RomEntry{CfgKey::Management_Agent_Offset, 0x000080, 1, 0}, + RomEntry{CfgKey::Unit_Characteristics, 0x080400, 0, 0}, + }; + + auto device = deviceManager.UpsertDevice(record, rom); + EXPECT_NE(nullptr, device); + if (!device) { + return; + } + EXPECT_FALSE(device->GetUnits().empty()); + } + + void AdvanceMs(uint64_t milliseconds) { + nowNs += milliseconds * 1'000'000ULL; + while (queue.DrainReadyForTesting() > 0U) { + } + } + + uint64_t CreateSession() { + auto result = registry.CreateSession(reinterpret_cast(0xCAFE), kGuid, 0); + EXPECT_TRUE(result.has_value()); + return result.value_or(0); + } + + void LoginSuccessfully(uint64_t handle, + uint16_t loginId = 0x0042, + uint32_t commandBlockAgentLo = 0x0020'0000) { + ASSERT_TRUE(registry.StartLogin(handle)); + ASSERT_EQ(1u, bus.PendingWriteCount()); + + const auto& loginWrite = bus.WriteAt(0); + const uint64_t loginOrbAddress = DecodeAddressFromWritePayload(loginWrite.data); + const uint64_t loginResponseAddress = + ReadORBAddress(addressManager, + loginOrbAddress, + offsetof(LoginORB, loginResponseAddressHi), + offsetof(LoginORB, loginResponseAddressLo)); + sessionStatusAddress = + ReadORBAddress(addressManager, + loginOrbAddress, + offsetof(LoginORB, statusFIFOAddressHi), + offsetof(LoginORB, statusFIFOAddressLo)); + + LoginResponse response{}; + response.length = ToBE16(LoginResponse::kSize); + response.loginID = ToBE16(loginId); + response.commandBlockAgentAddressHi = ToBE32(0x0000'FFFFu); + response.commandBlockAgentAddressLo = ToBE32(commandBlockAgentLo); + response.reconnectHold = ToBE16(1); + + ASSERT_TRUE(bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + addressManager.ApplyRemoteWrite( + loginResponseAddress, + std::span{reinterpret_cast(&response), + sizeof(response)}); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + addressManager.ApplyRemoteWrite( + sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + } + + static constexpr uint64_t kGuid = 0x0003DB0001DDDDA1ULL; + + ASFW::Async::Testing::DeferredFireWireBus bus; + AddressSpaceManager addressManager{nullptr}; + DeviceManager deviceManager; + IODispatchQueue queue; + SBP2SessionRegistry registry; + uint64_t nowNs{0}; + uint64_t sessionStatusAddress{0}; +}; + +TEST(SBP2SessionRegistryTests, BuildStandardCommandHelpersUseExpectedOpCodes) { + const auto inquiry = SCSI::BuildInquiryRequest(64); + ASSERT_EQ(6u, inquiry.cdb.size()); + EXPECT_EQ(0x12, inquiry.cdb[0]); + EXPECT_EQ(64u, inquiry.transferLength); + + const auto tur = SCSI::BuildTestUnitReadyRequest(); + ASSERT_EQ(6u, tur.cdb.size()); + EXPECT_EQ(0x00, tur.cdb[0]); + EXPECT_EQ(SCSI::DataDirection::None, tur.direction); + + const auto sense = SCSI::BuildRequestSenseRequest(18); + ASSERT_EQ(6u, sense.cdb.size()); + EXPECT_EQ(0x03, sense.cdb[0]); + EXPECT_EQ(SCSI::DataDirection::FromTarget, sense.direction); +} + +TEST(SBP2SessionRegistryTests, SubmitRequestSenseCapturesPayloadAndSenseData) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + const auto request = SCSI::BuildRequestSenseRequest(18); + const size_t pendingBeforeSubmit = rig.bus.PendingWriteCount(); + ASSERT_TRUE(rig.registry.SubmitCommand(handle, request)); + ASSERT_EQ(pendingBeforeSubmit + 1U, rig.bus.PendingWriteCount()); + + const auto& write = rig.bus.WriteAt(rig.bus.WriteCount() - 1); + const uint64_t commandOrbAddress = DecodeAddressFromWritePayload(write.data); + const uint64_t dataBufferAddress = + ReadDataBufferAddress(rig.addressManager, commandOrbAddress); + + const std::vector sensePayload{ + 0x70, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x0A, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x3A, 0x01 + }; + rig.addressManager.ApplyRemoteWrite(dataBufferAddress, sensePayload); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + status.orbOffsetHi = ToBE16(static_cast((commandOrbAddress >> 32) & 0xFFFFu)); + status.orbOffsetLo = ToBE32(static_cast(commandOrbAddress & 0xFFFF'FFFFu)); + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + auto result = rig.registry.GetCommandResult(handle); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(0, result->transportStatus); + EXPECT_EQ(SBPStatus::kNoAdditionalInfo, result->sbpStatus); + EXPECT_EQ(sensePayload, result->payload); + EXPECT_EQ(sensePayload, result->senseData); +} + +TEST(SBP2SessionRegistryTests, ActiveCommandFailsOnceAfterFetchAgentRetryExhaustion) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + auto* session = rig.registry.GetSessionForTesting(handle); + ASSERT_NE(nullptr, session); + + session->SetFetchAgentWriteRetriesForTesting(0); + const auto request = SCSI::BuildTestUnitReadyRequest(); + ASSERT_TRUE(rig.registry.SubmitCommand(handle, request)); + const auto fetchAgentWrite = rig.bus.WriteAt(rig.bus.WriteCount() - 1); + ASSERT_EQ(8u, fetchAgentWrite.data.size()); + + ASSERT_TRUE(rig.bus.CompleteWrite(fetchAgentWrite.handle, ASFW::Async::AsyncStatus::kTimeout)); + rig.AdvanceMs(1000); + while (rig.queue.DrainReadyForTesting() > 0U) { + } + + const auto commandResult = rig.registry.GetCommandResult(handle); + ASSERT_TRUE(commandResult.has_value()); + EXPECT_EQ(-1, commandResult->transportStatus); + EXPECT_EQ(SBPStatus::kUnspecifiedError, commandResult->sbpStatus); + + const auto stateAfterFailure = rig.registry.GetSessionState(handle); + ASSERT_TRUE(stateAfterFailure.has_value()); + EXPECT_EQ(-1, stateAfterFailure->lastError); + + ASSERT_GT(rig.bus.WriteCount(), 0u); + const auto agentResetWrite = rig.bus.WriteAt(rig.bus.WriteCount() - 1); + EXPECT_EQ(4u, agentResetWrite.data.size()); + + ASSERT_TRUE(rig.bus.CompleteWrite(agentResetWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); + while (rig.queue.DrainReadyForTesting() > 0U) { + } +} + +TEST(SBP2SessionRegistryTests, SubmitTaskManagementWritesLogicalUnitResetORB) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + const size_t writesBeforeSubmit = rig.bus.WriteCount(); + ASSERT_TRUE(rig.registry.SubmitTaskManagement( + handle, SBP2ManagementORB::Function::LogicalUnitReset)); + + ASSERT_EQ(writesBeforeSubmit + 1U, rig.bus.WriteCount()); + const auto& write = rig.bus.WriteAt(writesBeforeSubmit); + const uint64_t managementOrbAddress = DecodeAddressFromWritePayload(write.data); + + EXPECT_EQ(0x0Eu, ReadTaskManagementFunction(rig.addressManager, managementOrbAddress)); + EXPECT_EQ(0x0042u, ReadTaskManagementLoginID(rig.addressManager, managementOrbAddress)); +} + +TEST(SBP2SessionRegistryTests, TaskManagementSuccessClearsActiveCommandTracking) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + const auto request = SCSI::BuildTestUnitReadyRequest(); + ASSERT_TRUE(rig.registry.SubmitCommand(handle, request)); + ASSERT_FALSE(rig.registry.GetCommandResult(handle).has_value()); + ASSERT_GT(rig.bus.WriteCount(), 0U); + const auto fetchAgentWrite = rig.bus.WriteAt(rig.bus.WriteCount() - 1); + ASSERT_EQ(8U, fetchAgentWrite.data.size()); + + const size_t writesBeforeTaskManagement = rig.bus.WriteCount(); + ASSERT_TRUE(rig.registry.SubmitTaskManagement( + handle, SBP2ManagementORB::Function::AbortTaskSet)); + const auto& taskWrite = rig.bus.WriteAt(writesBeforeTaskManagement); + const uint64_t taskOrbAddress = DecodeAddressFromWritePayload(taskWrite.data); + const uint64_t taskStatusAddress = + ReadTaskManagementStatusAddress(rig.addressManager, taskOrbAddress); + + ASSERT_TRUE(rig.bus.CompleteWrite(taskWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); + CompleteTaskManagementStatus(rig.addressManager, taskStatusAddress, taskOrbAddress); + + EXPECT_FALSE(rig.registry.GetCommandResult(handle).has_value()); + EXPECT_FALSE(rig.bus.CompleteWrite(fetchAgentWrite.handle, + ASFW::Async::AsyncStatus::kSuccess)); + EXPECT_TRUE(rig.registry.SubmitCommand(handle, request)); +} + +TEST(SBP2SessionRegistryTests, SubmitTaskManagementRejectsInvalidFunction) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + EXPECT_FALSE(rig.registry.SubmitTaskManagement( + handle, SBP2ManagementORB::Function::QueryLogins)); +} + +TEST(SBP2SessionRegistryTests, MissingDiscoverySuspendsDeviceAndReconnectWaitsForResume) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + rig.registry.OnBusReset(2); + const auto suspendedState = rig.registry.GetSessionState(handle); + ASSERT_TRUE(suspendedState.has_value()); + EXPECT_EQ(ASFW::Protocols::SBP2::LoginState::Suspended, suspendedState->loginState); + + rig.deviceManager.MarkDeviceLost(SessionRegistryRig::kGuid); + const auto suspendedDevice = rig.deviceManager.GetDeviceByGUID(SessionRegistryRig::kGuid); + ASSERT_NE(nullptr, suspendedDevice); + EXPECT_TRUE(suspendedDevice->IsSuspended()); + + const size_t writesBeforeMissingRefresh = rig.bus.WriteCount(); + rig.registry.RefreshTargets(Generation{2}); + EXPECT_EQ(writesBeforeMissingRefresh, rig.bus.WriteCount()); + + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.UpsertDevice(Generation{2}, 0x33); + const auto resumedDevice = rig.deviceManager.GetDeviceByGUID(SessionRegistryRig::kGuid); + ASSERT_NE(nullptr, resumedDevice); + EXPECT_TRUE(resumedDevice->IsReady()); + EXPECT_EQ(0x33u, resumedDevice->GetNodeID()); + + rig.registry.RefreshTargets(Generation{2}); + ASSERT_EQ(writesBeforeMissingRefresh + 1U, rig.bus.WriteCount()); + const auto& reconnectWrite = rig.bus.WriteAt(writesBeforeMissingRefresh); + EXPECT_EQ(0x33u, reconnectWrite.nodeId.value); + EXPECT_EQ(8U, reconnectWrite.data.size()); + + const uint64_t reconnectOrbAddress = DecodeAddressFromWritePayload(reconnectWrite.data); + const uint64_t reconnectStatusAddress = + ReadORBAddress(rig.addressManager, + reconnectOrbAddress, + offsetof(ReconnectORB, statusFIFOAddressHi), + offsetof(ReconnectORB, statusFIFOAddressLo)); + ASSERT_TRUE(rig.bus.CompleteWrite(reconnectWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); + + StatusBlock reconnectStatus{}; + reconnectStatus.details = 0; + reconnectStatus.sbpStatus = SBPStatus::kNoAdditionalInfo; + rig.addressManager.ApplyRemoteWrite( + reconnectStatusAddress, + std::span{reinterpret_cast(&reconnectStatus), + sizeof(reconnectStatus)}); + + ASSERT_GE(rig.bus.WriteCount(), writesBeforeMissingRefresh + 2U); + const auto& busyTimeoutWrite = rig.bus.WriteAt(writesBeforeMissingRefresh + 1U); + EXPECT_EQ(0x33u, busyTimeoutWrite.nodeId.value); + EXPECT_EQ(0x33u, busyTimeoutWrite.address.nodeID); + + const size_t writesBeforeCommand = rig.bus.WriteCount(); + ASSERT_TRUE(rig.registry.SubmitCommand(handle, SCSI::BuildTestUnitReadyRequest())); + ASSERT_EQ(writesBeforeCommand + 1U, rig.bus.WriteCount()); + const auto& fetchAgentWrite = rig.bus.WriteAt(writesBeforeCommand); + EXPECT_EQ(0x33u, fetchAgentWrite.nodeId.value); + EXPECT_EQ(0x33u, fetchAgentWrite.address.nodeID); +} + +TEST(SBP2SessionRegistryTests, RepeatedMissingDiscoveryTerminatesSuspendedSBP2Device) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + rig.registry.OnBusReset(2); + rig.deviceManager.MarkDeviceLost(SessionRegistryRig::kGuid); + const auto suspendedDevice = rig.deviceManager.GetDeviceByGUID(SessionRegistryRig::kGuid); + ASSERT_NE(nullptr, suspendedDevice); + ASSERT_TRUE(suspendedDevice->IsSuspended()); + + rig.deviceManager.MarkDeviceLost(SessionRegistryRig::kGuid); + EXPECT_EQ(nullptr, rig.deviceManager.GetDeviceByGUID(SessionRegistryRig::kGuid)); + + const size_t writesBeforeRefresh = rig.bus.WriteCount(); + rig.registry.RefreshTargets(Generation{2}); + EXPECT_EQ(writesBeforeRefresh, rig.bus.WriteCount()); +} + +TEST(SBP2SessionRegistryTests, MissingDiscoveryStillTerminatesNonSBP2Device) { + DeviceManager deviceManager; + + DeviceRecord record{}; + record.guid = SessionRegistryRig::kGuid + 2U; + record.vendorId = 0x001122; + record.modelId = 0x334455; + record.kind = DeviceKind::Unknown; + record.vendorName = "Other Vendor"; + record.modelName = "Other Device"; + record.gen = Generation{1}; + record.nodeId = 0x10; + record.link = LinkPolicy{}; + record.state = LifeState::Ready; + + ConfigROM rom{}; + rom.gen = Generation{1}; + rom.nodeId = record.nodeId; + rom.rootDirMinimal = { + RomEntry{CfgKey::Unit_Spec_Id, 0x00A02D, 0, 0}, + RomEntry{CfgKey::Unit_Sw_Version, 0x010001, 0, 0}, + }; + + ASSERT_NE(nullptr, deviceManager.UpsertDevice(record, rom)); + deviceManager.MarkDeviceLost(record.guid); + EXPECT_EQ(nullptr, deviceManager.GetDeviceByGUID(record.guid)); +} + +TEST(SBP2SessionRegistryTests, SubmitCommandRejectsCDBLargerThanORBPayloadBudget) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + SCSI::CommandRequest request{}; + request.cdb = std::vector(16, 0x12); + request.direction = SCSI::DataDirection::None; + request.transferLength = 0; + request.timeoutMs = 100; + + EXPECT_FALSE(rig.registry.SubmitCommand(handle, request)); +} + +TEST(SBP2SessionRegistryTests, CreateSessionAcceptsRealSBP2SpecAndVersion) { + SessionRegistryRig rig; + auto result = rig.registry.CreateSession(reinterpret_cast(0xCAFE), + SessionRegistryRig::kGuid, + 0); + ASSERT_TRUE(result.has_value()); +} + +TEST(SBP2SessionRegistryTests, DeviceDiscoveryParsesNikonStyleManagementAgentCSRKey) { + DeviceManager deviceManager; + + DeviceRecord record{}; + record.guid = SessionRegistryRig::kGuid + 1U; + record.vendorId = 0x0090B5; + record.modelId = 0x004001; + record.kind = DeviceKind::Unknown; + record.vendorName = "Nikon"; + record.modelName = "LS-4000 ED"; + record.gen = Generation{1}; + record.nodeId = 0x00; + record.link = LinkPolicy{}; + record.state = LifeState::Ready; + + ConfigROM rom{}; + rom.gen = Generation{1}; + rom.nodeId = record.nodeId; + rom.bib.busInfoLength = 4; + rom.rootDirMinimal = { + RomEntry{CfgKey::Unit_Directory, 0x000001, 3, 1}, + }; + rom.rawQuadlets = { + ToBE32(0x04045343u), + ToBE32(0x31333934u), + ToBE32(0x00FF5012u), + ToBE32(0x0090B540u), + ToBE32(0x01FFFFFFu), + ToBE32(0x0001B344u), + ToBE32(0x0004CAEEu), + ToBE32(0x1200609Eu), + ToBE32(0x13010483u), + ToBE32(0x5400C000u), + ToBE32(0x14060000u), + }; + + auto device = deviceManager.UpsertDevice(record, rom); + ASSERT_NE(device, nullptr); + ASSERT_EQ(device->GetUnits().size(), 1u); + + const auto& unit = device->GetUnits().front(); + ASSERT_NE(unit, nullptr); + EXPECT_TRUE(unit->Matches(kSBP2UnitSpecId, kSBP2UnitSwVersion)); + ASSERT_TRUE(unit->GetManagementAgentOffset().has_value()); + EXPECT_EQ(*unit->GetManagementAgentOffset(), 0x00C000u); + ASSERT_TRUE(unit->GetLUN().has_value()); + EXPECT_EQ(*unit->GetLUN(), 0x060000u); +} + +} // namespace From 166b8a705908925bc5d4fc040dd984464d309a14 Mon Sep 17 00:00:00 2001 From: gly11 Date: Wed, 27 May 2026 21:51:54 +0800 Subject: [PATCH 02/22] fix(sbp2): harden command submission lifecycle --- .../Protocols/SBP2/SBP2LoginSession.cpp | 17 +- .../Protocols/SBP2/SBP2LoginSession.hpp | 1 + .../Protocols/SBP2/SBP2SessionRegistry.cpp | 269 ++++++++++-------- .../Protocols/SBP2/SBP2SessionRegistry.hpp | 2 +- tests/SBP2LoginSessionTests.cpp | 31 ++ 5 files changed, 191 insertions(+), 129 deletions(-) diff --git a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp index 69e76687..d687c608 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp @@ -1303,6 +1303,9 @@ bool SBP2LoginSession::SubmitORB(SBP2CommandORB* orb) noexcept { return false; } RingDoorbell(); + if (activeFetchAgentORB_ != orb) { + StartSubmittedORBTimer(orb); + } } return true; @@ -1356,8 +1359,17 @@ bool SBP2LoginSession::AppendORBImmediate(SBP2CommandORB* orb) noexcept { return true; } +void SBP2LoginSession::StartSubmittedORBTimer(SBP2CommandORB* orb) noexcept { + if (orb == nullptr) { + return; + } + + IODispatchQueue* timeoutQueue = timeoutQueue_ != nullptr ? timeoutQueue_ : workQueue_; + orb->StartTimer(workQueue_, timeoutQueue); +} + void SBP2LoginSession::FailActiveCommandIfPresent(int transportStatus, - uint8_t sbpStatus) noexcept { + uint8_t sbpStatus) noexcept { if (activeFetchAgentORB_ == nullptr) { return; } @@ -1500,7 +1512,8 @@ void SBP2LoginSession::OnFetchAgentWriteComplete(uint16_t expectedGeneration, return; } - // Fetch agent write succeeded. Submit deferred ORB if any. + // Fetch agent write succeeded. The target can now fetch this ORB. + StartSubmittedORBTimer(activeFetchAgentORB_); activeFetchAgentORB_ = nullptr; if (!pendingImmediateORBs_.empty()) { diff --git a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp index af4d4b3d..70c0834c 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp @@ -406,6 +406,7 @@ class SBP2LoginSession { /// Write ORB address to fetch agent (CBA + kORBPointer). bool AppendORBImmediate(SBP2CommandORB* orb) noexcept; + void StartSubmittedORBTimer(SBP2CommandORB* orb) noexcept; void FailActiveCommandIfPresent(int transportStatus, uint8_t sbpStatus) noexcept; void FailSubmittedORB(SBP2CommandORB* orb, int transportStatus, diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp index 97d6a937..a09f5d5e 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp @@ -90,15 +90,17 @@ SBP2SessionRegistry::SBP2SessionRegistry(Async::IFireWireBus& bus, } SBP2SessionRegistry::~SBP2SessionRegistry() { - IOLockGuard lock(lock_); - for (auto& [handle, record] : sessions_) { - if (record.session && record.session->State() == LoginState::LoggedIn) { - (void)record.session->Logout(); + { + IOLockGuard lock(lock_); + for (auto& [handle, record] : sessions_) { + if (record.session && record.session->State() == LoginState::LoggedIn) { + (void)record.session->Logout(); + } + CleanupCommandResources(record); + CleanupManagementResources(record); } - CleanupCommandResources(record); - CleanupManagementResources(record); + sessions_.clear(); } - sessions_.clear(); if (lock_ != nullptr) { IOLockFree(lock_); @@ -134,7 +136,7 @@ std::expected SBP2SessionRegistry::CreateSession(void* owner, return std::unexpected(kIOReturnUnsupported); } - auto session = std::make_unique(bus_, busInfo_, addrSpaceMgr_); + auto session = std::make_shared(bus_, busInfo_, addrSpaceMgr_); session->Configure(targetInfo); session->SetWorkQueue(workQueue_); @@ -242,146 +244,161 @@ std::optional> SBP2SessionRegistry::GetInquiryResult(uint64 } bool SBP2SessionRegistry::SubmitCommand(uint64_t handle, const SCSI::CommandRequest& request) { - IOLockGuard lock(lock_); - auto* record = FindByHandle(handle); - if (!record || !record->session || request.cdb.empty()) { - return false; - } + std::shared_ptr session; + SBP2CommandORB* submittedORB = nullptr; - if (record->session->State() != LoginState::LoggedIn || record->commandInFlight) { - return false; - } + { + IOLockGuard lock(lock_); + auto* record = FindByHandle(handle); + if (!record || !record->session || request.cdb.empty()) { + return false; + } - if (request.transferLength > 0 && request.direction == SCSI::DataDirection::None) { - return false; - } - if (request.direction == SCSI::DataDirection::FromTarget && !request.outgoingPayload.empty()) { - return false; - } - if (request.direction == SCSI::DataDirection::ToTarget && - request.outgoingPayload.size() != request.transferLength) { - return false; - } + if (record->session->State() != LoginState::LoggedIn || record->commandInFlight) { + return false; + } - const uint16_t maxCDB = record->session->TargetInfo().maxCommandBlockSize; - if (maxCDB < request.cdb.size()) { - return false; - } + if (request.transferLength > 0 && request.direction == SCSI::DataDirection::None) { + return false; + } + if (request.direction == SCSI::DataDirection::FromTarget && !request.outgoingPayload.empty()) { + return false; + } + if (request.direction == SCSI::DataDirection::ToTarget && + request.outgoingPayload.size() != request.transferLength) { + return false; + } - uint64_t bufferHandle = 0; - AddressSpaceManager::AddressRangeMeta bufferMeta{}; - if (request.transferLength > 0) { - const kern_return_t kr = addrSpaceMgr_.AllocateAddressRangeAuto( - record->owner, 0xFFFF, request.transferLength, &bufferHandle, &bufferMeta); - if (kr != kIOReturnSuccess) { - ASFW_LOG(SBP2, "SBP2SessionRegistry: failed to allocate command buffer: 0x%08x", kr); + const uint16_t maxCDB = record->session->TargetInfo().maxCommandBlockSize; + if (maxCDB < request.cdb.size()) { return false; } - } - std::unique_ptr pageTable; - if (request.transferLength > 0) { - if (request.direction == SCSI::DataDirection::ToTarget) { - const kern_return_t writeKr = addrSpaceMgr_.WriteLocalData( - record->owner, - bufferHandle, - 0, - std::span{request.outgoingPayload.data(), request.outgoingPayload.size()}); - if (writeKr != kIOReturnSuccess) { + uint64_t bufferHandle = 0; + AddressSpaceManager::AddressRangeMeta bufferMeta{}; + if (request.transferLength > 0) { + const kern_return_t kr = addrSpaceMgr_.AllocateAddressRangeAuto( + record->owner, 0xFFFF, request.transferLength, &bufferHandle, &bufferMeta); + if (kr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2SessionRegistry: failed to allocate command buffer: 0x%08x", kr); + return false; + } + } + + std::unique_ptr pageTable; + if (request.transferLength > 0) { + if (request.direction == SCSI::DataDirection::ToTarget) { + const kern_return_t writeKr = addrSpaceMgr_.WriteLocalData( + record->owner, + bufferHandle, + 0, + std::span{request.outgoingPayload.data(), request.outgoingPayload.size()}); + if (writeKr != kIOReturnSuccess) { + addrSpaceMgr_.DeallocateAddressRange(record->owner, bufferHandle); + return false; + } + } + + pageTable = std::make_unique(addrSpaceMgr_, record->owner); + SBP2PageTable::Segment segment{bufferMeta.address, request.transferLength}; + if (!pageTable->Build(std::span(&segment, 1), + busInfo_.GetLocalNodeID().value)) { addrSpaceMgr_.DeallocateAddressRange(record->owner, bufferHandle); return false; } } - pageTable = std::make_unique(addrSpaceMgr_, record->owner); - SBP2PageTable::Segment segment{bufferMeta.address, request.transferLength}; - if (!pageTable->Build(std::span(&segment, 1), - busInfo_.GetLocalNodeID().value)) { - addrSpaceMgr_.DeallocateAddressRange(record->owner, bufferHandle); - return false; + auto orb = std::make_unique(addrSpaceMgr_, record->owner, maxCDB); + orb->SetCommandBlock(std::span{request.cdb.data(), request.cdb.size()}); + orb->SetFlags(BuildCommandFlags(request.direction)); + orb->SetMaxPayloadSize(record->session->MaxPayloadSize()); + orb->SetTimeout(request.timeoutMs > 0 + ? request.timeoutMs + : record->session->TargetInfo().managementTimeoutMs); + if (pageTable) { + orb->SetDataDescriptor(pageTable->GetResult()); } - } - auto orb = std::make_unique(addrSpaceMgr_, record->owner, maxCDB); - orb->SetCommandBlock(std::span{request.cdb.data(), request.cdb.size()}); - orb->SetFlags(BuildCommandFlags(request.direction)); - orb->SetMaxPayloadSize(record->session->MaxPayloadSize()); - orb->SetTimeout(request.timeoutMs > 0 - ? request.timeoutMs - : record->session->TargetInfo().managementTimeoutMs); - if (pageTable) { - orb->SetDataDescriptor(pageTable->GetResult()); - } + const uint64_t captureHandle = handle; + orb->SetCompletionCallback([this, captureHandle](int transportStatus, uint8_t sbpStatus) { + IOLockGuard cbLock(lock_); + auto* rec = FindByHandle(captureHandle); + if (rec == nullptr) { + return; + } + if (!rec->commandInFlight) { + return; + } - const uint64_t captureHandle = handle; - orb->SetCompletionCallback([this, captureHandle](int transportStatus, uint8_t sbpStatus) { - IOLockGuard cbLock(lock_); - auto* rec = FindByHandle(captureHandle); - if (rec == nullptr) { - return; - } - if (!rec->commandInFlight) { - return; - } + rec->commandInFlight = false; + rec->commandReady = true; + rec->lastCompletedCommandOpcode = rec->activeCommandOpcode; + rec->activeCommandOpcode.reset(); - rec->commandInFlight = false; - rec->commandReady = true; - rec->lastCompletedCommandOpcode = rec->activeCommandOpcode; - rec->activeCommandOpcode.reset(); - - SCSI::CommandResult result{}; - result.transportStatus = transportStatus; - result.sbpStatus = sbpStatus; - - if (transportStatus == 0 && - sbpStatus == Wire::SBPStatus::kNoAdditionalInfo && - rec->activeCommandRequest.has_value() && - rec->activeCommandRequest->direction == SCSI::DataDirection::FromTarget && - rec->activeCommandRequest->transferLength > 0 && - rec->commandBufferHandle != 0) { - std::vector payload; - const kern_return_t readKr = addrSpaceMgr_.ReadIncomingData( - rec->owner, - rec->commandBufferHandle, - 0, - rec->activeCommandRequest->transferLength, - &payload); - if (readKr == kIOReturnSuccess) { - result.payload = std::move(payload); - } else { - result.transportStatus = static_cast(readKr); + SCSI::CommandResult result{}; + result.transportStatus = transportStatus; + result.sbpStatus = sbpStatus; + + if (transportStatus == 0 && + sbpStatus == Wire::SBPStatus::kNoAdditionalInfo && + rec->activeCommandRequest.has_value() && + rec->activeCommandRequest->direction == SCSI::DataDirection::FromTarget && + rec->activeCommandRequest->transferLength > 0 && + rec->commandBufferHandle != 0) { + std::vector payload; + const kern_return_t readKr = addrSpaceMgr_.ReadIncomingData( + rec->owner, + rec->commandBufferHandle, + 0, + rec->activeCommandRequest->transferLength, + &payload); + if (readKr == kIOReturnSuccess) { + result.payload = std::move(payload); + } else { + result.transportStatus = static_cast(readKr); + } } - } - if (rec->activeCommandRequest.has_value() && rec->activeCommandRequest->captureSenseData) { - result.senseData = result.payload; - } + if (rec->activeCommandRequest.has_value() && rec->activeCommandRequest->captureSenseData) { + result.senseData = result.payload; + } - rec->state.lastError = static_cast(result.transportStatus); - if (result.transportStatus == 0 && result.sbpStatus == Wire::SBPStatus::kNoAdditionalInfo) { - rec->state.lastError = 0; - } - rec->pendingCommandResult = std::move(result); - rec->activeCommandRequest.reset(); - CleanupCommandResources(*rec); - }); + rec->state.lastError = static_cast(result.transportStatus); + if (result.transportStatus == 0 && result.sbpStatus == Wire::SBPStatus::kNoAdditionalInfo) { + rec->state.lastError = 0; + } + rec->pendingCommandResult = std::move(result); + rec->activeCommandRequest.reset(); + CleanupCommandResources(*rec); + }); - if (!record->session->SubmitORB(orb.get())) { - if (bufferHandle != 0) { - addrSpaceMgr_.DeallocateAddressRange(record->owner, bufferHandle); - } - return false; + record->commandInFlight = true; + record->commandReady = false; + record->pendingCommandResult.reset(); + record->activeCommandRequest = request; + record->activeCommandOpcode = request.cdb.front(); + record->commandBufferHandle = bufferHandle; + record->commandORB = std::move(orb); + record->commandPageTable = std::move(pageTable); + session = record->session; + submittedORB = record->commandORB.get(); } - record->commandInFlight = true; - record->commandReady = false; - record->pendingCommandResult.reset(); - record->activeCommandRequest = request; - record->activeCommandOpcode = request.cdb.front(); - record->commandBufferHandle = bufferHandle; - record->commandORB = std::move(orb); - record->commandPageTable = std::move(pageTable); - return true; + if (session->SubmitORB(submittedORB)) { + return true; + } + + IOLockGuard lock(lock_); + auto* record = FindByHandle(handle); + if (record != nullptr && record->commandORB.get() == submittedORB) { + record->commandInFlight = false; + record->commandReady = false; + record->pendingCommandResult.reset(); + record->activeCommandRequest.reset(); + record->activeCommandOpcode.reset(); + CleanupCommandResources(*record); + } + return false; } std::optional SBP2SessionRegistry::GetCommandResult(uint64_t handle) { diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp index bc9c951f..39639a7f 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp @@ -41,7 +41,7 @@ struct SBP2SessionRecord { void* owner{nullptr}; uint64_t guid{0}; uint32_t romOffset{0}; - std::unique_ptr session; + std::shared_ptr session; SBP2SessionState state{}; std::optional activeCommandRequest; diff --git a/tests/SBP2LoginSessionTests.cpp b/tests/SBP2LoginSessionTests.cpp index 2ed4da11..14a74693 100644 --- a/tests/SBP2LoginSessionTests.cpp +++ b/tests/SBP2LoginSessionTests.cpp @@ -417,6 +417,37 @@ TEST(SBP2LoginSessionTests, ImmediateORBRetryStaysBoundToOriginalORBAndQueuesNex EXPECT_EQ(firstAddressLo, retryAddressLo); } +TEST(SBP2LoginSessionTests, SubmittedImmediateORBStartsTimeoutAfterFetchAgentWriteSucceeds) { + SessionRig rig; + rig.LoginSuccessfully(); + + SBP2CommandORB orb(rig.addressManager, &rig.session, 16); + orb.SetFlags(SBP2CommandORB::kImmediate); + orb.SetTimeout(25); + + int callbackStatus = 99; + int callbackCount = 0; + orb.SetCompletionCallback([&](int status, uint8_t) { + ++callbackCount; + callbackStatus = status; + }); + + const size_t pendingTimersBeforeSubmit = rig.timeoutQueue.PendingTaskCountForTesting(); + + ASSERT_TRUE(rig.session.SubmitORB(&orb)); + EXPECT_EQ(pendingTimersBeforeSubmit, rig.timeoutQueue.PendingTaskCountForTesting()); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + EXPECT_EQ(pendingTimersBeforeSubmit + 1U, rig.timeoutQueue.PendingTaskCountForTesting()); + + rig.AdvanceMs(24); + EXPECT_EQ(0, callbackCount); + + rig.AdvanceMs(1); + EXPECT_EQ(1, callbackCount); + EXPECT_EQ(-1, callbackStatus); +} + TEST(SBP2LoginSessionTests, SolicitedStatusCompletesORBMatchingByORBAddress) { SessionRig rig; rig.LoginSuccessfully(); From c1c3ec5523448e1503eff716c228beb4da40d01c Mon Sep 17 00:00:00 2001 From: gly11 Date: Thu, 28 May 2026 13:37:28 +0800 Subject: [PATCH 03/22] fix(sbp2): correct status block wire format --- ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp | 38 ++++++++++--------- tests/SBP2ORBTests.cpp | 26 +++++++++++++ 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp b/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp index 806df769..c363d1ae 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp @@ -193,11 +193,11 @@ static_assert(offsetof(PageTableEntry, segmentBaseAddressLo) == 4, "PTE addressL // --------------------------------------------------------------------------- // SBP-2 Status Block -// Ref: SBP-2 §5.2 +// Ref: SBP-2 §5.3 status blocks // --------------------------------------------------------------------------- struct StatusBlock { - uint8_t details{0}; // [7] Src, [6:4] Resp, [3:2] D, [1:0] Len + uint8_t details{0}; // [7:6] Src, [5:4] Resp, [3] D, [2:0] Len uint8_t sbpStatus{0}; // SBP-2 specific status code uint16_t orbOffsetHi{0}; uint32_t orbOffsetLo{0}; @@ -205,10 +205,10 @@ struct StatusBlock { static constexpr uint32_t kMaxSize = 32; // header (8) + max status (24) - [[nodiscard]] uint8_t Source() const noexcept { return (details >> 7) & 0x1; } - [[nodiscard]] uint8_t Response() const noexcept { return (details >> 4) & 0x7; } - [[nodiscard]] uint8_t DeadBit() const noexcept { return (details >> 2) & 0x1; } - [[nodiscard]] uint8_t Length() const noexcept { return details & 0x3; } + [[nodiscard]] uint8_t Source() const noexcept { return (details >> 6) & 0x3; } + [[nodiscard]] uint8_t Response() const noexcept { return (details >> 4) & 0x3; } + [[nodiscard]] uint8_t DeadBit() const noexcept { return (details >> 3) & 0x1; } + [[nodiscard]] uint8_t Length() const noexcept { return details & 0x7; } }; static_assert(sizeof(StatusBlock) == 32, "StatusBlock must be 32 bytes"); @@ -315,19 +315,21 @@ namespace Options { static constexpr uint32_t kFunctionTargetReset = 0xF; } -// SBP-2 status codes (from sbpStatus field) +// SBP-2 §5.3.1 request status codes (from sbpStatus field) namespace SBPStatus { - static constexpr uint8_t kNoAdditionalInfo = 0; - static constexpr uint8_t kReqTypeNotSupported = 1; - static constexpr uint8_t kSpeedNotSupported = 2; - static constexpr uint8_t kPageSizeNotSupported = 3; - static constexpr uint8_t kAccessDenied = 4; - static constexpr uint8_t kResourceUnavailable = 5; - static constexpr uint8_t kFunctionRejected = 6; - static constexpr uint8_t kLoginIDNotRecognized = 7; - static constexpr uint8_t kDummyORBCompleted = 8; - static constexpr uint8_t kRequestAborted = 0xB; - static constexpr uint8_t kUnspecifiedError = 0xFF; + static constexpr uint8_t kNoAdditionalInfo = 0; + static constexpr uint8_t kReqTypeNotSupported = 1; + static constexpr uint8_t kSpeedNotSupported = 2; + static constexpr uint8_t kPageSizeNotSupported = 3; + static constexpr uint8_t kAccessDenied = 4; + static constexpr uint8_t kLogicalUnitNotSupported = 5; + static constexpr uint8_t kMaxPayloadTooSmall = 6; + static constexpr uint8_t kResourcesUnavailable = 8; + static constexpr uint8_t kFunctionRejected = 9; + static constexpr uint8_t kLoginIDNotRecognized = 10; + static constexpr uint8_t kDummyORBCompleted = 11; + static constexpr uint8_t kRequestAborted = 12; + static constexpr uint8_t kUnspecifiedError = 0xFF; } // Busy timeout register (CSR address 0xFFFFF0000210) diff --git a/tests/SBP2ORBTests.cpp b/tests/SBP2ORBTests.cpp index 524aabae..d4a567e2 100644 --- a/tests/SBP2ORBTests.cpp +++ b/tests/SBP2ORBTests.cpp @@ -89,6 +89,32 @@ class ORBTimerRig { uint64_t nowNs{0}; }; +TEST(SBP2ORBTests, StatusBlockDetailsDecodeSBP2Layout) { + StatusBlock status{}; + status.details = 0b10011101; + + EXPECT_EQ(2, status.Source()); + EXPECT_EQ(1, status.Response()); + EXPECT_EQ(1, status.DeadBit()); + EXPECT_EQ(5, status.Length()); +} + +TEST(SBP2ORBTests, SBPStatusConstantsMatchSBP2RequestStatus) { + EXPECT_EQ(0, SBPStatus::kNoAdditionalInfo); + EXPECT_EQ(1, SBPStatus::kReqTypeNotSupported); + EXPECT_EQ(2, SBPStatus::kSpeedNotSupported); + EXPECT_EQ(3, SBPStatus::kPageSizeNotSupported); + EXPECT_EQ(4, SBPStatus::kAccessDenied); + EXPECT_EQ(5, SBPStatus::kLogicalUnitNotSupported); + EXPECT_EQ(6, SBPStatus::kMaxPayloadTooSmall); + EXPECT_EQ(8, SBPStatus::kResourcesUnavailable); + EXPECT_EQ(9, SBPStatus::kFunctionRejected); + EXPECT_EQ(10, SBPStatus::kLoginIDNotRecognized); + EXPECT_EQ(11, SBPStatus::kDummyORBCompleted); + EXPECT_EQ(12, SBPStatus::kRequestAborted); + EXPECT_EQ(0xFF, SBPStatus::kUnspecifiedError); +} + TEST(SBP2ORBTests, CommandORBTimerFiresOnHostQueue) { ORBTimerRig rig; From 575a23f6fc0b324778d338dd4dd77470c886302d Mon Sep 17 00:00:00 2001 From: gly11 Date: Thu, 28 May 2026 13:48:59 +0800 Subject: [PATCH 04/22] fix(sbp2): harden command ORB configuration --- ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp | 16 +++++++--- ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp | 4 +-- .../Protocols/SBP2/SBP2SessionRegistry.cpp | 8 +++-- tests/SBP2ORBTests.cpp | 31 +++++++++++++++++++ 4 files changed, 50 insertions(+), 9 deletions(-) diff --git a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp index 56b9a609..26e25947 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp @@ -65,13 +65,16 @@ void SBP2CommandORB::DeallocateResources() noexcept { // Command block (CDB) // --------------------------------------------------------------------------- -void SBP2CommandORB::SetCommandBlock(std::span cdb) noexcept { +bool SBP2CommandORB::SetCommandBlock(std::span cdb) noexcept { if (!IsValid() || orbStorage_.size() < Wire::NormalORB::kHeaderSize) { - return; + return false; + } + + if (cdb.size() > static_cast(maxCommandBlockSize_)) { + return false; } - const uint32_t copyLen = static_cast( - std::min(cdb.size(), static_cast(maxCommandBlockSize_))); + const uint32_t copyLen = static_cast(cdb.size()); if (copyLen > 0) { std::memcpy(orbStorage_.data() + Wire::NormalORB::kHeaderSize, @@ -83,6 +86,8 @@ void SBP2CommandORB::SetCommandBlock(std::span cdb) noexcept { std::memset(orbStorage_.data() + Wire::NormalORB::kHeaderSize + copyLen, 0, maxCommandBlockSize_ - copyLen); } + + return true; } // --------------------------------------------------------------------------- @@ -95,6 +100,9 @@ kern_return_t SBP2CommandORB::PrepareForExecution(uint16_t localNodeID, if (!IsValid() || orbStorage_.size() < sizeof(Wire::NormalORB)) { return kIOReturnNotReady; } + if (maxPayloadLog > 15) { + return kIOReturnBadArgument; + } auto* orb = reinterpret_cast(orbStorage_.data()); const uint16_t busNodeID = Wire::NormalizeBusNodeID(localNodeID); diff --git a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp index 98e6658a..1f637630 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp @@ -50,9 +50,8 @@ class SBP2CommandORB { SBP2CommandORB& operator=(const SBP2CommandORB&) = delete; // Configuration (call before submit) - void SetCommandBlock(std::span cdb) noexcept; + [[nodiscard]] bool SetCommandBlock(std::span cdb) noexcept; void SetFlags(uint32_t flags) noexcept { flags_ = flags; } - void SetMaxPayloadSize(uint16_t bytes) noexcept { maxPayloadSize_ = bytes; } void SetTimeout(uint32_t ms) noexcept { timeoutDuration_ = ms; } void SetCompletionCallback(CompletionCallback cb) noexcept { completionCallback_ = std::move(cb); @@ -109,7 +108,6 @@ class SBP2CommandORB { uint32_t maxCommandBlockSize_; uint32_t flags_{0}; - uint16_t maxPayloadSize_{0}; uint32_t timeoutDuration_{0}; CompletionCallback completionCallback_; diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp index a09f5d5e..781514ed 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp @@ -309,9 +309,13 @@ bool SBP2SessionRegistry::SubmitCommand(uint64_t handle, const SCSI::CommandRequ } auto orb = std::make_unique(addrSpaceMgr_, record->owner, maxCDB); - orb->SetCommandBlock(std::span{request.cdb.data(), request.cdb.size()}); + if (!orb->SetCommandBlock(std::span{request.cdb.data(), request.cdb.size()})) { + if (bufferHandle != 0) { + addrSpaceMgr_.DeallocateAddressRange(record->owner, bufferHandle); + } + return false; + } orb->SetFlags(BuildCommandFlags(request.direction)); - orb->SetMaxPayloadSize(record->session->MaxPayloadSize()); orb->SetTimeout(request.timeoutMs > 0 ? request.timeoutMs : record->session->TargetInfo().managementTimeoutMs); diff --git a/tests/SBP2ORBTests.cpp b/tests/SBP2ORBTests.cpp index d4a567e2..83ed54ff 100644 --- a/tests/SBP2ORBTests.cpp +++ b/tests/SBP2ORBTests.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace { @@ -129,6 +130,36 @@ TEST(SBP2ORBTests, CommandORBTimerFiresOnHostQueue) { EXPECT_EQ(-1, completionStatus); } +TEST(SBP2ORBTests, CommandORBRejectsOversizedCDBWithoutOverwritingCommandBlock) { + ORBTimerRig rig; + + SBP2CommandORB orb(rig.addressManager, reinterpret_cast(0x8), 4); + const std::array originalCDB{0x12, 0x34, 0x56, 0x78}; + const std::array oversizedCDB{0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA}; + + ASSERT_TRUE(orb.SetCommandBlock(originalCDB)); + EXPECT_FALSE(orb.SetCommandBlock(oversizedCDB)); + ASSERT_EQ(kIOReturnSuccess, orb.PrepareForExecution(0x21, ASFW::FW::FwSpeed::S400, 6)); + + const auto orbAddress = orb.GetORBAddress(); + const uint64_t packedAddress = ComposeAddress(orbAddress.addressHi, orbAddress.addressLo); + const uint32_t commandQuadlet = ReadQuadlet( + rig.addressManager, + packedAddress + ASFW::Protocols::SBP2::Wire::NormalORB::kHeaderSize); + + std::array writtenCDB{}; + std::memcpy(writtenCDB.data(), &commandQuadlet, writtenCDB.size()); + EXPECT_EQ(originalCDB, writtenCDB); +} + +TEST(SBP2ORBTests, CommandORBRejectsOutOfRangeMaxPayloadLog) { + ORBTimerRig rig; + + SBP2CommandORB orb(rig.addressManager, reinterpret_cast(0x9), 4); + + EXPECT_EQ(kIOReturnBadArgument, orb.PrepareForExecution(0x21, ASFW::FW::FwSpeed::S400, 16)); +} + TEST(SBP2ORBTests, CommandORBCancelSuppressesPendingTimeout) { ORBTimerRig rig; From 26bf1fae926454f5c53a529f3723aaddc03a1042 Mon Sep 17 00:00:00 2001 From: gly11 Date: Thu, 28 May 2026 13:57:49 +0800 Subject: [PATCH 05/22] fix(sbp2): start reconnect timeout after ACK --- .../Protocols/SBP2/SBP2LoginSession.cpp | 9 +++++++ .../Protocols/SBP2/SBP2LoginSession.hpp | 1 + tests/SBP2LoginSessionTests.cpp | 24 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp index d687c608..3b3b4764 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp @@ -735,6 +735,7 @@ void SBP2LoginSession::OnReconnectWriteComplete(uint16_t expectedGeneration, // Reconnect ORB write ACK'd. Wait for status block from device. ASFW_LOG(SBP2, "SBP2LoginSession: reconnect write ACK'd, waiting for status block"); + StartReconnectTimer(); } void SBP2LoginSession::OnReconnectTimeout() noexcept { @@ -832,6 +833,7 @@ void SBP2LoginSession::OnStatusBlockRemoteWrite(uint32_t offset, case LoginState::Reconnecting: reconnectTimerActive_ = false; + CancelPendingTimer(); CompleteReconnectFromStatusBlock(block, len); break; @@ -1075,6 +1077,13 @@ void SBP2LoginSession::StartLoginTimer() noexcept { }); } +void SBP2LoginSession::StartReconnectTimer() noexcept { + reconnectTimerActive_ = true; + SubmitDelayedCallback(targetInfo_.managementTimeoutMs + 1000, [this]() { + OnReconnectTimeout(); + }); +} + void SBP2LoginSession::StartLogoutTimer() noexcept { logoutTimerActive_ = true; SubmitDelayedCallback(targetInfo_.managementTimeoutMs, [this]() { diff --git a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp index 70c0834c..8aaae04d 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp @@ -284,6 +284,7 @@ class SBP2LoginSession { void SetState(LoginState newState) noexcept; void StartLoginTimer() noexcept; + void StartReconnectTimer() noexcept; void StartLogoutTimer() noexcept; void CancelLoginTimer() noexcept; diff --git a/tests/SBP2LoginSessionTests.cpp b/tests/SBP2LoginSessionTests.cpp index 14a74693..1f2c3539 100644 --- a/tests/SBP2LoginSessionTests.cpp +++ b/tests/SBP2LoginSessionTests.cpp @@ -289,6 +289,30 @@ TEST(SBP2LoginSessionTests, ReconnectReplaysBusyTimeoutRegister) { ExpectBusyTimeoutWrite(rig.bus.WriteAt(reconnectWriteIndex + 1), kTargetNodeID); } +TEST(SBP2LoginSessionTests, ReconnectAckStartsStatusTimeoutAndFallsBackToLogin) { + SessionRig rig; + rig.LoginSuccessfully(); + + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + ASSERT_EQ(LoginState::Suspended, rig.session.State()); + ASSERT_TRUE(rig.session.Reconnect()); + ASSERT_EQ(LoginState::Reconnecting, rig.session.State()); + + const size_t reconnectWriteIndex = rig.bus.WriteCount() - 1; + ASSERT_TRUE(rig.bus.CompleteWrite(rig.bus.WriteAt(reconnectWriteIndex).handle, + ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); + + rig.AdvanceMs(1009); + EXPECT_EQ(LoginState::Reconnecting, rig.session.State()); + EXPECT_EQ(reconnectWriteIndex + 1, rig.bus.WriteCount()); + + rig.AdvanceMs(1); + EXPECT_EQ(LoginState::LoggingIn, rig.session.State()); + EXPECT_EQ(reconnectWriteIndex + 2, rig.bus.WriteCount()); +} + TEST(SBP2LoginSessionTests, BusyTimeoutReplayCancelsInFlightWrite) { SessionRig rig; From 26d98f27eb564472a81bef538fa97deb70f55aa8 Mon Sep 17 00:00:00 2001 From: gly11 Date: Thu, 28 May 2026 14:05:04 +0800 Subject: [PATCH 06/22] fix(sbp2): release page table ranges on destruction --- ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp | 4 +++ tests/SBP2ORBTests.cpp | 32 +++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp b/ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp index 4967a3e2..ffa9463b 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp @@ -35,6 +35,10 @@ class SBP2PageTable { explicit SBP2PageTable(AddressSpaceManager& addrMgr, void* owner) noexcept : addrMgr_(addrMgr), owner_(owner) {} + ~SBP2PageTable() noexcept { + Clear(); + } + SBP2PageTable(const SBP2PageTable&) = delete; SBP2PageTable& operator=(const SBP2PageTable&) = delete; diff --git a/tests/SBP2ORBTests.cpp b/tests/SBP2ORBTests.cpp index 83ed54ff..48905545 100644 --- a/tests/SBP2ORBTests.cpp +++ b/tests/SBP2ORBTests.cpp @@ -277,6 +277,38 @@ TEST(SBP2ORBTests, CommandORBDirectDescriptorUsesFullBusNodeId) { EXPECT_EQ((static_cast(expectedNode) << 16) | 0xFFFFu, dataDescriptorHi); } +TEST(SBP2ORBTests, PageTableDestructorReleasesAddressRange) { + ORBTimerRig rig; + + uint64_t pageTableAddress = 0; + { + ASFW::Protocols::SBP2::SBP2PageTable pageTable( + rig.addressManager, reinterpret_cast(0x10)); + ASFW::Protocols::SBP2::SBP2PageTable::Segment segment{ + .address = 0x2000, + .length = 0xF004, + }; + + ASSERT_TRUE(pageTable.Build( + std::span(&segment, 1), + 0x21)); + ASSERT_FALSE(pageTable.GetResult().isDirect); + + const uint32_t descriptorHi = FromBE32(pageTable.GetResult().dataDescriptorHi); + const uint32_t descriptorLo = FromBE32(pageTable.GetResult().dataDescriptorLo); + pageTableAddress = ComposeAddress(static_cast(descriptorHi & 0xFFFFu), + descriptorLo); + + uint32_t pteQuadlet = 0; + EXPECT_EQ(ASFW::Async::ResponseCode::Complete, + rig.addressManager.ReadQuadlet(pageTableAddress, &pteQuadlet)); + } + + uint32_t pteQuadlet = 0; + EXPECT_EQ(ASFW::Async::ResponseCode::AddressError, + rig.addressManager.ReadQuadlet(pageTableAddress, &pteQuadlet)); +} + TEST(SBP2ORBTests, ManagementORBDestructionInvalidatesPendingTimeout) { ORBTimerRig rig; From 81c6172a96efdb1089aa6e761c1dd8218c49f76c Mon Sep 17 00:00:00 2001 From: gly11 Date: Thu, 28 May 2026 14:06:26 +0800 Subject: [PATCH 07/22] fix(sbp2): decode unit characteristics fields --- .../Protocols/SBP2/SBP2LoginSession.hpp | 4 ++-- .../Protocols/SBP2/SBP2SessionRegistry.cpp | 4 ++-- tests/SBP2SessionRegistryTests.cpp | 21 +++++++++++++++---- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp index 8aaae04d..5c79dbe7 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp @@ -49,8 +49,8 @@ struct SBP2TargetInfo { uint16_t lun{0}; // Logical unit number // From Unit_Characteristics key (if present) - uint32_t managementTimeoutMs{2000}; // (byte[1] of unitCharacteristics) * 500 ms - uint16_t maxORBSize{32}; // (byte[0] * 4), min 32 + uint32_t managementTimeoutMs{2000}; // Unit_Characteristics[15:8] * 500 ms + uint16_t maxORBSize{32}; // Unit_Characteristics[7:0] * 4, min 32 uint16_t maxCommandBlockSize{0}; // maxORBSize - sizeof(NormalORB header) // From Fast_Start key (optional) diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp index 781514ed..d55318ed 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp @@ -51,8 +51,8 @@ SBP2TargetInfo BuildTargetInfoFromUnit(const Discovery::FWUnit& unit) { if (auto uc = unit.GetUnitCharacteristics(); uc.has_value()) { const uint32_t value = *uc; - const uint8_t orbSizeUnits = static_cast((value >> 24) & 0xFF); - const uint8_t timeoutUnits = static_cast((value >> 16) & 0xFF); + const uint8_t orbSizeUnits = static_cast(value & 0xFF); + const uint8_t timeoutUnits = static_cast((value >> 8) & 0xFF); info.managementTimeoutMs = static_cast(timeoutUnits) * 500; info.maxORBSize = std::max(static_cast(orbSizeUnits) * 4, 32); } diff --git a/tests/SBP2SessionRegistryTests.cpp b/tests/SBP2SessionRegistryTests.cpp index 2f9df3de..143f29fc 100644 --- a/tests/SBP2SessionRegistryTests.cpp +++ b/tests/SBP2SessionRegistryTests.cpp @@ -115,7 +115,7 @@ void CompleteTaskManagementStatus(AddressSpaceManager& manager, class SessionRegistryRig { public: - SessionRegistryRig() + explicit SessionRegistryRig(uint32_t unitCharacteristics = 0x080400) : registry(bus, bus, addressManager, deviceManager, &queue) { queue.SetManualDispatchForTesting(true); ASFW::Testing::SetHostMonotonicClockForTesting([this]() { return nowNs; }); @@ -124,14 +124,14 @@ class SessionRegistryRig { bus.SetLocalNodeID(ASFW::FW::NodeId{0x2A}); bus.SetDefaultSpeed(ASFW::FW::FwSpeed::S400); - UpsertDevice(Generation{1}, 0x32); + UpsertDevice(Generation{1}, 0x32, unitCharacteristics); } ~SessionRegistryRig() { ASFW::Testing::ResetHostMonotonicClockForTesting(); } - void UpsertDevice(Generation generation, uint8_t nodeId) { + void UpsertDevice(Generation generation, uint8_t nodeId, uint32_t unitCharacteristics = 0x080400) { DeviceRecord record{}; record.guid = kGuid; record.vendorId = 0x001122; @@ -154,7 +154,7 @@ class SessionRegistryRig { RomEntry{CfgKey::Unit_Sw_Version, kSBP2UnitSwVersion, 0, 0}, RomEntry{CfgKey::Logical_Unit_Number, 0x000002, 0, 0}, RomEntry{CfgKey::Management_Agent_Offset, 0x000080, 1, 0}, - RomEntry{CfgKey::Unit_Characteristics, 0x080400, 0, 0}, + RomEntry{CfgKey::Unit_Characteristics, unitCharacteristics, 0, 0}, }; auto device = deviceManager.UpsertDevice(record, rom); @@ -506,6 +506,19 @@ TEST(SBP2SessionRegistryTests, CreateSessionAcceptsRealSBP2SpecAndVersion) { ASSERT_TRUE(result.has_value()); } +TEST(SBP2SessionRegistryTests, UnitCharacteristicsDecodeTimeoutAndORBSizeFromLowBytes) { + SessionRegistryRig rig(0x000410); + + const uint64_t handle = rig.CreateSession(); + auto* session = rig.registry.GetSessionForTesting(handle); + ASSERT_NE(nullptr, session); + + const auto& targetInfo = session->TargetInfo(); + EXPECT_EQ(2000u, targetInfo.managementTimeoutMs); + EXPECT_EQ(64u, targetInfo.maxORBSize); + EXPECT_EQ(64u - NormalORB::kHeaderSize, targetInfo.maxCommandBlockSize); +} + TEST(SBP2SessionRegistryTests, DeviceDiscoveryParsesNikonStyleManagementAgentCSRKey) { DeviceManager deviceManager; From 81643081a3c8285b7f2355f0a1026b72a5ac492d Mon Sep 17 00:00:00 2001 From: gly11 Date: Thu, 28 May 2026 14:22:24 +0800 Subject: [PATCH 08/22] fix(sbp2): guard session async callbacks with weak ownership --- .../Protocols/SBP2/SBP2LoginSession.cpp | 238 +++++++++++++----- .../Protocols/SBP2/SBP2LoginSession.hpp | 2 +- .../Protocols/SBP2/SBP2SessionRegistry.cpp | 85 ++++++- .../Protocols/SBP2/SBP2SessionRegistry.hpp | 7 + tests/SBP2LoginSessionTests.cpp | 6 +- tests/SBP2SessionRegistryTests.cpp | 63 ++++- 6 files changed, 334 insertions(+), 67 deletions(-) diff --git a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp index 3b3b4764..b29bd278 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp @@ -25,6 +25,38 @@ SBP2LoginSession::SBP2LoginSession(Async::IFireWireBus& bus, SBP2LoginSession::~SBP2LoginSession() { CancelPendingTimer(); + if (loginWriteHandle_) { + bus_.Cancel(loginWriteHandle_); + loginWriteHandle_ = {}; + } + if (reconnectWriteHandle_) { + bus_.Cancel(reconnectWriteHandle_); + reconnectWriteHandle_ = {}; + } + if (logoutWriteHandle_) { + bus_.Cancel(logoutWriteHandle_); + logoutWriteHandle_ = {}; + } + if (fetchAgentWriteHandle_) { + bus_.Cancel(fetchAgentWriteHandle_); + fetchAgentWriteHandle_ = {}; + } + if (doorbellWriteHandle_) { + bus_.Cancel(doorbellWriteHandle_); + doorbellWriteHandle_ = {}; + } + if (agentResetWriteHandle_) { + bus_.Cancel(agentResetWriteHandle_); + agentResetWriteHandle_ = {}; + } + if (unsolicitedStatusWriteHandle_) { + bus_.Cancel(unsolicitedStatusWriteHandle_); + unsolicitedStatusWriteHandle_ = {}; + } + if (busyTimeoutWriteHandle_) { + bus_.Cancel(busyTimeoutWriteHandle_); + busyTimeoutWriteHandle_ = {}; + } ClearORBTracking(true); lifetimeToken_.reset(); ReleaseOwnedTimeoutQueue(); @@ -110,13 +142,16 @@ bool SBP2LoginSession::Login() noexcept { }; const FW::FwSpeed speed = busInfo_.GetSpeed(node); + const std::weak_ptr weakSelf = weak_from_this(); loginWriteHandle_ = bus_.WriteBlock( gen, node, mgmtAddr, std::span{loginORBAddressBE_.data(), loginORBAddressBE_.size()}, speed, - [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, - std::span response) { - OnLoginWriteComplete(requestGeneration, status, response); + [weakSelf, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + if (auto self = weakSelf.lock()) { + self->OnLoginWriteComplete(requestGeneration, status, response); + } }); if (!loginWriteHandle_) { @@ -156,13 +191,16 @@ bool SBP2LoginSession::Logout() noexcept { }; const FW::FwSpeed speed = busInfo_.GetSpeed(node); + const std::weak_ptr weakSelf = weak_from_this(); logoutWriteHandle_ = bus_.WriteBlock( gen, node, mgmtAddr, std::span{logoutORBAddressBE_.data(), logoutORBAddressBE_.size()}, speed, - [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, - std::span response) { - OnLogoutWriteComplete(requestGeneration, status, response); + [weakSelf, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + if (auto self = weakSelf.lock()) { + self->OnLogoutWriteComplete(requestGeneration, status, response); + } }); if (!logoutWriteHandle_) { @@ -212,18 +250,26 @@ bool SBP2LoginSession::Reconnect() noexcept { }; const FW::FwSpeed speed = busInfo_.GetSpeed(node); + const std::weak_ptr weakSelf = weak_from_this(); reconnectWriteHandle_ = bus_.WriteBlock( gen, node, mgmtAddr, std::span{reconnectORBAddressBE_.data(), reconnectORBAddressBE_.size()}, speed, - [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, - std::span response) { - OnReconnectWriteComplete(requestGeneration, status, response); + [weakSelf, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + if (auto self = weakSelf.lock()) { + self->OnReconnectWriteComplete(requestGeneration, status, response); + } }); if (!reconnectWriteHandle_) { ASFW_LOG(SBP2, "SBP2LoginSession::Reconnect: WriteBlock failed, will retry"); - SubmitDelayedCallback(kLoginRetryDelayMs, [this]() { OnReconnectTimeout(); }); + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(kLoginRetryDelayMs, [weakSelf]() { + if (auto self = weakSelf.lock()) { + self->OnReconnectTimeout(); + } + }); reconnectTimerActive_ = true; return true; // Will retry } @@ -249,7 +295,14 @@ void SBP2LoginSession::HandleBusReset(uint16_t newGeneration) noexcept { ClearORBTracking(true); DeallocateResources(); SetState(LoginState::Idle); - SubmitDelayedCallback(100, [this]() { (void)Login(); }); + { + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(100, [weakSelf]() { + if (auto self = weakSelf.lock()) { + (void)self->Login(); + } + }); + } break; case LoginState::LoggedIn: @@ -269,7 +322,14 @@ void SBP2LoginSession::HandleBusReset(uint16_t newGeneration) noexcept { DeallocateResources(); loginGeneration_ = newGeneration; SetState(LoginState::Suspended); - SubmitDelayedCallback(100, [this]() { (void)Reconnect(); }); + { + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(100, [weakSelf]() { + if (auto self = weakSelf.lock()) { + (void)self->Reconnect(); + } + }); + } break; case LoginState::LoggingOut: @@ -320,10 +380,13 @@ bool SBP2LoginSession::AllocateResources() noexcept { if (allResourcesAllocated) { // Re-register callback in case it was previously cleared by a reset path. if (statusBlockHandle_ != 0) { + const std::weak_ptr weakSelf = weak_from_this(); addrSpaceMgr_.SetRemoteWriteCallback( statusBlockHandle_, - [this](uint64_t /*handle*/, uint32_t offset, std::span payload) { - OnStatusBlockRemoteWrite(offset, payload); + [weakSelf](uint64_t /*handle*/, uint32_t offset, std::span payload) { + if (auto self = weakSelf.lock()) { + self->OnStatusBlockRemoteWrite(offset, payload); + } }); } return true; // Already allocated @@ -368,10 +431,13 @@ bool SBP2LoginSession::AllocateResources() noexcept { // Register a callback for status block writes — the device writes status // here to signal login/reconnect/logout completion (and ORB completion // in Step 2). + const std::weak_ptr weakSelf = weak_from_this(); addrSpaceMgr_.SetRemoteWriteCallback( statusBlockHandle_, - [this](uint64_t /*handle*/, uint32_t offset, std::span payload) { - OnStatusBlockRemoteWrite(offset, payload); + [weakSelf](uint64_t /*handle*/, uint32_t offset, std::span payload) { + if (auto self = weakSelf.lock()) { + self->OnStatusBlockRemoteWrite(offset, payload); + } }); ASFW_LOG(SBP2, "SBP2LoginSession: all address spaces allocated"); @@ -649,11 +715,15 @@ void SBP2LoginSession::OnLoginWriteComplete(uint16_t expectedGeneration, if (loginRetryCount_ < kLoginRetryMax) { loginRetryCount_++; - SubmitDelayedCallback(kLoginRetryDelayMs, [this]() { - loginGeneration_ = static_cast(busInfo_.GetGeneration().value); - loginNodeID_ = targetInfo_.targetNodeId; - SetState(LoginState::Idle); - (void)Login(); + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(kLoginRetryDelayMs, [weakSelf]() { + if (auto self = weakSelf.lock()) { + self->loginGeneration_ = + static_cast(self->busInfo_.GetGeneration().value); + self->loginNodeID_ = self->targetInfo_.targetNodeId; + self->SetState(LoginState::Idle); + (void)self->Login(); + } }); return; } @@ -729,7 +799,12 @@ void SBP2LoginSession::OnReconnectWriteComplete(uint16_t expectedGeneration, ASFW_LOG(SBP2, "SBP2LoginSession::OnReconnectWriteComplete: status=%s, retrying", Async::ToString(status)); - SubmitDelayedCallback(100, [this]() { (void)Reconnect(); }); + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(100, [weakSelf]() { + if (auto self = weakSelf.lock()) { + (void)self->Reconnect(); + } + }); return; } @@ -781,6 +856,7 @@ void SBP2LoginSession::OnLogoutWriteComplete(uint16_t expectedGeneration, } ASFW_LOG(SBP2, "SBP2LoginSession: logout management agent write ACK'd, waiting for status block"); + StartLogoutTimer(); } void SBP2LoginSession::OnLogoutTimeout() noexcept { @@ -863,11 +939,15 @@ void SBP2LoginSession::CompleteLoginFromStatusBlock(const Wire::StatusBlock& blo if (loginRetryCount_ < kLoginRetryMax) { loginRetryCount_++; - SubmitDelayedCallback(kLoginRetryDelayMs, [this]() { - loginGeneration_ = static_cast(busInfo_.GetGeneration().value); - loginNodeID_ = targetInfo_.targetNodeId; - SetState(LoginState::Idle); - (void)Login(); + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(kLoginRetryDelayMs, [weakSelf]() { + if (auto self = weakSelf.lock()) { + self->loginGeneration_ = + static_cast(self->busInfo_.GetGeneration().value); + self->loginNodeID_ = self->targetInfo_.targetNodeId; + self->SetState(LoginState::Idle); + (void)self->Login(); + } }); return; } @@ -897,11 +977,15 @@ void SBP2LoginSession::CompleteLoginFromStatusBlock(const Wire::StatusBlock& blo if (loginRetryCount_ < kLoginRetryMax) { loginRetryCount_++; - SubmitDelayedCallback(kLoginRetryDelayMs, [this]() { - loginGeneration_ = static_cast(busInfo_.GetGeneration().value); - loginNodeID_ = targetInfo_.targetNodeId; - SetState(LoginState::Idle); - (void)Login(); + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(kLoginRetryDelayMs, [weakSelf]() { + if (auto self = weakSelf.lock()) { + self->loginGeneration_ = + static_cast(self->busInfo_.GetGeneration().value); + self->loginNodeID_ = self->targetInfo_.targetNodeId; + self->SetState(LoginState::Idle); + (void)self->Login(); + } }); return; } @@ -1072,22 +1156,31 @@ void SBP2LoginSession::SetState(LoginState newState) noexcept { void SBP2LoginSession::StartLoginTimer() noexcept { loginTimerActive_ = true; - SubmitDelayedCallback(targetInfo_.managementTimeoutMs, [this]() { - OnLoginTimeout(); + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(targetInfo_.managementTimeoutMs, [weakSelf]() { + if (auto self = weakSelf.lock()) { + self->OnLoginTimeout(); + } }); } void SBP2LoginSession::StartReconnectTimer() noexcept { reconnectTimerActive_ = true; - SubmitDelayedCallback(targetInfo_.managementTimeoutMs + 1000, [this]() { - OnReconnectTimeout(); + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(targetInfo_.managementTimeoutMs + 1000, [weakSelf]() { + if (auto self = weakSelf.lock()) { + self->OnReconnectTimeout(); + } }); } void SBP2LoginSession::StartLogoutTimer() noexcept { logoutTimerActive_ = true; - SubmitDelayedCallback(targetInfo_.managementTimeoutMs, [this]() { - OnLogoutTimeout(); + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(targetInfo_.managementTimeoutMs, [weakSelf]() { + if (auto self = weakSelf.lock()) { + self->OnLogoutTimeout(); + } }); } @@ -1163,13 +1256,14 @@ void SBP2LoginSession::SubmitDelayedCallback(uint64_t delayMs, const uint64_t expectedGeneration = delayedCallbackGeneration_.fetch_add(1, std::memory_order_acq_rel) + 1ULL; const std::weak_ptr weakLifetime = lifetimeToken_; + const std::weak_ptr weakSelf = weak_from_this(); const uint64_t delayNs = delayMs * 1'000'000ULL; IODispatchQueue* bounceQueue = workQueue_; DispatchAfterCompat(delayQueue, delayNs, - [this, - weakLifetime, + [weakLifetime, + weakSelf, expectedGeneration, bounceQueue, cb = std::move(callback)]() mutable { @@ -1177,14 +1271,18 @@ void SBP2LoginSession::SubmitDelayedCallback(uint64_t delayMs, return; } DispatchAsyncCompat(bounceQueue, - [this, - weakLifetime, + [weakLifetime, + weakSelf, expectedGeneration, cb = std::move(cb)]() mutable { if (weakLifetime.expired()) { return; } - if (delayedCallbackGeneration_.load(std::memory_order_acquire) != expectedGeneration) { + auto self = weakSelf.lock(); + if (!self) { + return; + } + if (self->delayedCallbackGeneration_.load(std::memory_order_acquire) != expectedGeneration) { return; } cb(); @@ -1345,13 +1443,16 @@ bool SBP2LoginSession::AppendORBImmediate(SBP2CommandORB* orb) noexcept { const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; const FW::FwSpeed speed = busInfo_.GetSpeed(node); + const std::weak_ptr weakSelf = weak_from_this(); fetchAgentWriteHandle_ = bus_.WriteBlock( gen, node, fetchAgentAddress_, std::span{fetchAgentWriteData_.data(), fetchAgentWriteData_.size()}, speed, - [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, - std::span response) { - OnFetchAgentWriteComplete(requestGeneration, status, response); + [weakSelf, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + if (auto self = weakSelf.lock()) { + self->OnFetchAgentWriteComplete(requestGeneration, status, response); + } }); if (!fetchAgentWriteHandle_) { @@ -1464,11 +1565,14 @@ void SBP2LoginSession::RingDoorbell() noexcept { const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; const FW::FwSpeed speed = busInfo_.GetSpeed(node); + const std::weak_ptr weakSelf = weak_from_this(); doorbellWriteHandle_ = bus_.WriteQuad( gen, node, doorbellAddress_, 0, speed, - [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, - std::span response) { - OnDoorbellComplete(requestGeneration, status, response); + [weakSelf, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + if (auto self = weakSelf.lock()) { + self->OnDoorbellComplete(requestGeneration, status, response); + } }); if (!doorbellWriteHandle_) { @@ -1500,9 +1604,12 @@ void SBP2LoginSession::OnFetchAgentWriteComplete(uint16_t expectedGeneration, activeFetchAgentORB_->SetFetchAgentWriteRetries(retries); // Retry after a delay SBP2CommandORB* retryORB = activeFetchAgentORB_; - SubmitDelayedCallback(1000, [this, retryORB]() { - if (activeFetchAgentORB_ == retryORB) { - AppendORBImmediate(retryORB); + const std::weak_ptr weakSelf = weak_from_this(); + SubmitDelayedCallback(1000, [weakSelf, retryORB]() { + if (auto self = weakSelf.lock()) { + if (self->activeFetchAgentORB_ == retryORB) { + self->AppendORBImmediate(retryORB); + } } }); return; @@ -1607,11 +1714,14 @@ void SBP2LoginSession::ResetFetchAgent(std::function callback) noexce const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; const FW::FwSpeed speed = busInfo_.GetSpeed(node); + const std::weak_ptr weakSelf = weak_from_this(); agentResetWriteHandle_ = bus_.WriteQuad( gen, node, agentResetAddress_, 0, speed, - [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, - std::span response) { - OnAgentResetComplete(requestGeneration, status, response); + [weakSelf, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + if (auto self = weakSelf.lock()) { + self->OnAgentResetComplete(requestGeneration, status, response); + } }); if (!agentResetWriteHandle_) { @@ -1661,11 +1771,14 @@ void SBP2LoginSession::EnableUnsolicitedStatus() noexcept { const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; const FW::FwSpeed speed = busInfo_.GetSpeed(node); + const std::weak_ptr weakSelf = weak_from_this(); unsolicitedStatusWriteHandle_ = bus_.WriteQuad( gen, node, unsolicitedStatusAddress_, 0, speed, - [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, - std::span response) { - OnUnsolicitedStatusEnableComplete(requestGeneration, status, response); + [weakSelf, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + if (auto self = weakSelf.lock()) { + self->OnUnsolicitedStatusEnableComplete(requestGeneration, status, response); + } }); } @@ -1705,15 +1818,18 @@ void SBP2LoginSession::WriteBusyTimeout() noexcept { const FW::FwSpeed speed = busInfo_.GetSpeed(node); busyTimeoutInProgress_ = true; + const std::weak_ptr weakSelf = weak_from_this(); busyTimeoutWriteHandle_ = bus_.WriteBlock( gen, node, busyAddr, std::span{reinterpret_cast(&busyTimeoutBuffer_), 4}, speed, - [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, - std::span response) { - OnBusyTimeoutComplete(requestGeneration, status, response); + [weakSelf, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + if (auto self = weakSelf.lock()) { + self->OnBusyTimeoutComplete(requestGeneration, status, response); + } }); if (!busyTimeoutWriteHandle_) { diff --git a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp index 5c79dbe7..8e19fc28 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp @@ -110,7 +110,7 @@ enum class LoginState : uint8_t { // SBP2LoginSession // --------------------------------------------------------------------------- -class SBP2LoginSession { +class SBP2LoginSession : public std::enable_shared_from_this { friend class SBP2SessionRegistry; public: diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp index d55318ed..3c2d9b70 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp @@ -101,6 +101,7 @@ SBP2SessionRegistry::~SBP2SessionRegistry() { } sessions_.clear(); } + retiringSessions_.clear(); if (lock_ != nullptr) { IOLockFree(lock_); @@ -488,10 +489,16 @@ bool SBP2SessionRegistry::ReleaseSession(uint64_t handle) { (record.session->State() == LoginState::LoggedIn || record.session->State() == LoginState::Suspended)) { waitMs = std::max(record.session->TargetInfo().managementTimeoutMs + 500, 500); + SetReleaseLogoutCallbackLocked(handle, record.session); if (!record.session->Logout()) { sessions_.erase(it); return true; } + } else if (record.session && record.session->State() == LoginState::LoggingOut) { + SetReleaseLogoutCallbackLocked(handle, record.session); + RetireSessionLocked(record.session); + sessions_.erase(it); + return true; } else { sessions_.erase(it); return true; @@ -521,6 +528,9 @@ bool SBP2SessionRegistry::ReleaseSession(uint64_t handle) { if (it != sessions_.end()) { CleanupCommandResources(it->second); CleanupManagementResources(it->second); + if (it->second.session && it->second.session->State() == LoginState::LoggingOut) { + RetireSessionLocked(it->second.session); + } sessions_.erase(it); } return true; @@ -531,8 +541,24 @@ void SBP2SessionRegistry::ReleaseOwner(void* owner) { for (auto it = sessions_.begin(); it != sessions_.end();) { if (it->second.owner == owner) { auto& record = it->second; - if (record.session && record.session->State() == LoginState::LoggedIn) { - (void)record.session->Logout(); + if (record.session && + (record.session->State() == LoginState::LoggedIn || + record.session->State() == LoginState::Suspended)) { + SetReleaseLogoutCallbackLocked(record.handle, record.session); + if (record.session->Logout()) { + CleanupCommandResources(record); + CleanupManagementResources(record); + RetireSessionLocked(record.session); + it = sessions_.erase(it); + continue; + } + } else if (record.session && record.session->State() == LoginState::LoggingOut) { + SetReleaseLogoutCallbackLocked(record.handle, record.session); + CleanupCommandResources(record); + CleanupManagementResources(record); + RetireSessionLocked(record.session); + it = sessions_.erase(it); + continue; } CleanupCommandResources(record); CleanupManagementResources(record); @@ -629,6 +655,15 @@ SBP2LoginSession* SBP2SessionRegistry::GetSessionForTesting(uint64_t handle) { } return record->session.get(); } + +std::weak_ptr SBP2SessionRegistry::GetSessionWeakForTesting(uint64_t handle) { + IOLockGuard lock(lock_); + auto* record = FindByHandle(handle); + if (!record) { + return {}; + } + return record->session; +} #endif SBP2SessionRecord* SBP2SessionRegistry::FindByHandle(uint64_t handle) { @@ -674,6 +709,52 @@ void SBP2SessionRegistry::CleanupManagementResources(SBP2SessionRecord& record) record.managementORB.reset(); } +void SBP2SessionRegistry::RetireSessionLocked(std::shared_ptr session) { + if (session == nullptr) { + return; + } + + if (std::find(retiringSessions_.begin(), retiringSessions_.end(), session) == + retiringSessions_.end()) { + retiringSessions_.push_back(std::move(session)); + } +} + +void SBP2SessionRegistry::EraseRetiredSessionLocked( + const std::shared_ptr& session) { + if (session == nullptr) { + return; + } + + retiringSessions_.erase( + std::remove(retiringSessions_.begin(), retiringSessions_.end(), session), + retiringSessions_.end()); +} + +void SBP2SessionRegistry::SetReleaseLogoutCallbackLocked( + uint64_t handle, + const std::shared_ptr& session) { + if (session == nullptr) { + return; + } + + std::weak_ptr weakSession = session; + session->SetLogoutCallback([this, handle, weakSession](const LogoutCompleteParams&) { + IOLockGuard cbLock(lock_); + std::shared_ptr completedSession = weakSession.lock(); + + auto it = sessions_.find(handle); + if (it != sessions_.end() && + (completedSession == nullptr || it->second.session == completedSession)) { + CleanupCommandResources(it->second); + CleanupManagementResources(it->second); + sessions_.erase(it); + } + + EraseRetiredSessionLocked(completedSession); + }); +} + bool SBP2SessionRegistry::IsSupportedTaskManagementFunction( SBP2ManagementORB::Function function) noexcept { switch (function) { diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp index 39639a7f..12fc7a52 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp @@ -112,6 +112,7 @@ class SBP2SessionRegistry { #ifdef ASFW_HOST_TEST // Testing hook: return the underlying login session for a handle. SBP2LoginSession* GetSessionForTesting(uint64_t handle); + std::weak_ptr GetSessionWeakForTesting(uint64_t handle); #endif private: @@ -126,6 +127,10 @@ class SBP2SessionRegistry { void CleanupCommandResources(SBP2SessionRecord& record); void CleanupManagementResources(SBP2SessionRecord& record); + void RetireSessionLocked(std::shared_ptr session); + void EraseRetiredSessionLocked(const std::shared_ptr& session); + void SetReleaseLogoutCallbackLocked(uint64_t handle, + const std::shared_ptr& session); [[nodiscard]] static bool IsSupportedTaskManagementFunction( SBP2ManagementORB::Function function) noexcept; @@ -137,6 +142,8 @@ class SBP2SessionRegistry { IOLock* lock_{nullptr}; std::map sessions_; + // Hidden from registry clients, but retained until async logout finishes or times out. + std::vector> retiringSessions_; uint64_t nextHandle_{1}; }; diff --git a/tests/SBP2LoginSessionTests.cpp b/tests/SBP2LoginSessionTests.cpp index 1f2c3539..637ddd19 100644 --- a/tests/SBP2LoginSessionTests.cpp +++ b/tests/SBP2LoginSessionTests.cpp @@ -80,7 +80,8 @@ uint64_t ReadORBAddress(AddressSpaceManager& manager, class SessionRig { public: SessionRig() - : session(bus, bus, addressManager) { + : sessionOwner(std::make_shared(bus, bus, addressManager)) + , session(*sessionOwner) { workQueue.SetManualDispatchForTesting(true); timeoutQueue.SetManualDispatchForTesting(true); ASFW::Testing::SetHostMonotonicClockForTesting([this]() { return nowNs; }); @@ -167,7 +168,8 @@ class SessionRig { ASFW::Async::Testing::DeferredFireWireBus bus; AddressSpaceManager addressManager{nullptr}; - SBP2LoginSession session; + std::shared_ptr sessionOwner; + SBP2LoginSession& session; IODispatchQueue workQueue; IODispatchQueue timeoutQueue; uint64_t nowNs{0}; diff --git a/tests/SBP2SessionRegistryTests.cpp b/tests/SBP2SessionRegistryTests.cpp index 143f29fc..5d10296c 100644 --- a/tests/SBP2SessionRegistryTests.cpp +++ b/tests/SBP2SessionRegistryTests.cpp @@ -174,7 +174,11 @@ class SessionRegistryRig { uint64_t CreateSession() { auto result = registry.CreateSession(reinterpret_cast(0xCAFE), kGuid, 0); EXPECT_TRUE(result.has_value()); - return result.value_or(0); + const uint64_t handle = result.value_or(0); + if (auto* session = registry.GetSessionForTesting(handle)) { + session->SetTimeoutQueue(&queue); + } + return handle; } void LoginSuccessfully(uint64_t handle, @@ -456,6 +460,63 @@ TEST(SBP2SessionRegistryTests, RepeatedMissingDiscoveryTerminatesSuspendedSBP2De EXPECT_EQ(writesBeforeRefresh, rig.bus.WriteCount()); } +TEST(SBP2SessionRegistryTests, ReleaseOwnerRetainsSessionUntilLogoutStatusArrives) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + std::weak_ptr weakSession = + rig.registry.GetSessionWeakForTesting(handle); + ASSERT_FALSE(weakSession.expired()); + + const size_t writesBeforeRelease = rig.bus.WriteCount(); + rig.registry.ReleaseOwner(reinterpret_cast(0xCAFE)); + + EXPECT_FALSE(weakSession.expired()); + ASSERT_EQ(writesBeforeRelease + 1U, rig.bus.WriteCount()); + EXPECT_FALSE(rig.registry.GetSessionState(handle).has_value()); + + const auto& logoutWrite = rig.bus.WriteAt(writesBeforeRelease); + EXPECT_TRUE(rig.bus.CompleteWrite(logoutWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); + EXPECT_FALSE(weakSession.expired()); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + EXPECT_TRUE(weakSession.expired()); + EXPECT_FALSE(rig.registry.GetSessionState(handle).has_value()); +} + +TEST(SBP2SessionRegistryTests, ReleaseOwnerRetainsSessionUntilLogoutTimeout) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + std::weak_ptr weakSession = + rig.registry.GetSessionWeakForTesting(handle); + ASSERT_FALSE(weakSession.expired()); + + const size_t writesBeforeRelease = rig.bus.WriteCount(); + rig.registry.ReleaseOwner(reinterpret_cast(0xCAFE)); + + EXPECT_FALSE(weakSession.expired()); + ASSERT_EQ(writesBeforeRelease + 1U, rig.bus.WriteCount()); + EXPECT_FALSE(rig.registry.GetSessionState(handle).has_value()); + + const auto& logoutWrite = rig.bus.WriteAt(writesBeforeRelease); + EXPECT_TRUE(rig.bus.CompleteWrite(logoutWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); + EXPECT_FALSE(weakSession.expired()); + + rig.AdvanceMs(2'000); + + EXPECT_TRUE(weakSession.expired()); + EXPECT_FALSE(rig.registry.GetSessionState(handle).has_value()); +} + TEST(SBP2SessionRegistryTests, MissingDiscoveryStillTerminatesNonSBP2Device) { DeviceManager deviceManager; From 138c7f10ddd9e65150c5443754cd4c684d893a6c Mon Sep 17 00:00:00 2001 From: gly11 Date: Thu, 28 May 2026 14:59:03 +0800 Subject: [PATCH 09/22] fix(sbp2): release failed command orb resources --- ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp | 16 ++-------------- ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp | 9 --------- .../Protocols/SBP2/SBP2SessionRegistry.cpp | 18 ++---------------- .../Protocols/SBP2/SBP2SessionRegistry.hpp | 3 +-- tests/SBP2SessionRegistryTests.cpp | 5 +++++ 5 files changed, 10 insertions(+), 41 deletions(-) diff --git a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp index b29bd278..ea759e92 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp @@ -1136,7 +1136,7 @@ void SBP2LoginSession::ProcessStatusBlock(const Wire::StatusBlock& block, outstandingORBs_.erase(it); if (orb != nullptr) { orb->CancelTimer(); - auto& cb = orb->GetCompletionCallback(); + auto cb = orb->GetCompletionCallback(); if (cb) { cb(0, block.sbpStatus); } @@ -1478,17 +1478,6 @@ void SBP2LoginSession::StartSubmittedORBTimer(SBP2CommandORB* orb) noexcept { orb->StartTimer(workQueue_, timeoutQueue); } -void SBP2LoginSession::FailActiveCommandIfPresent(int transportStatus, - uint8_t sbpStatus) noexcept { - if (activeFetchAgentORB_ == nullptr) { - return; - } - - if (activeCommandFailureCallback_) { - activeCommandFailureCallback_(transportStatus, sbpStatus); - } -} - void SBP2LoginSession::FailSubmittedORB(SBP2CommandORB* orb, int transportStatus, uint8_t sbpStatus) noexcept { @@ -1510,7 +1499,7 @@ void SBP2LoginSession::FailSubmittedORB(SBP2CommandORB* orb, orb->CancelTimer(); orb->SetAppended(false); - auto& cb = orb->GetCompletionCallback(); + auto cb = orb->GetCompletionCallback(); if (cb) { cb(transportStatus, sbpStatus); } @@ -1618,7 +1607,6 @@ void SBP2LoginSession::OnFetchAgentWriteComplete(uint16_t expectedGeneration, // Retries exhausted — report failure SBP2CommandORB* failedORB = activeFetchAgentORB_; if (failedORB != nullptr) { - FailActiveCommandIfPresent(-1, Wire::SBPStatus::kUnspecifiedError); FailSubmittedORB(failedORB, -1, Wire::SBPStatus::kUnspecifiedError); } FailPendingImmediateORBs(-1, Wire::SBPStatus::kUnspecifiedError); diff --git a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp index 8e19fc28..54e5a280 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp @@ -117,8 +117,6 @@ class SBP2LoginSession : public std::enable_shared_from_this { using LoginCallback = std::function; using LogoutCallback = std::function; using StatusCallback = std::function; - using ActiveCommandFailureCallback = - std::function; SBP2LoginSession(Async::IFireWireBus& bus, Async::IFireWireBusInfo& busInfo, @@ -144,11 +142,6 @@ class SBP2LoginSession : public std::enable_shared_from_this { /// Set status block notification callback (receives solicited + unsolicited status). void SetStatusCallback(StatusCallback cb) noexcept { statusCallback_ = std::move(cb); } - /// Set callback for unrecoverable active command failure in fetch-agent path. - void SetActiveCommandFailureCallback(ActiveCommandFailureCallback cb) noexcept { - activeCommandFailureCallback_ = std::move(cb); - } - /// Bind the IODispatchQueue used for delayed callbacks (timers). /// Must be called before Login() for timeout/retry support. void SetWorkQueue(IODispatchQueue* queue) noexcept; @@ -376,7 +369,6 @@ class SBP2LoginSession : public std::enable_shared_from_this { LoginCallback loginCallback_; LogoutCallback logoutCallback_; StatusCallback statusCallback_; - ActiveCommandFailureCallback activeCommandFailureCallback_; uint32_t testFetchAgentWriteRetries_{20}; // ----------------------------------------------------------------------- @@ -408,7 +400,6 @@ class SBP2LoginSession : public std::enable_shared_from_this { /// Write ORB address to fetch agent (CBA + kORBPointer). bool AppendORBImmediate(SBP2CommandORB* orb) noexcept; void StartSubmittedORBTimer(SBP2CommandORB* orb) noexcept; - void FailActiveCommandIfPresent(int transportStatus, uint8_t sbpStatus) noexcept; void FailSubmittedORB(SBP2CommandORB* orb, int transportStatus, uint8_t sbpStatus) noexcept; diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp index 3c2d9b70..f08f45b7 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp @@ -143,15 +143,6 @@ std::expected SBP2SessionRegistry::CreateSession(void* owner, IOLockGuard lock(lock_); const uint64_t handle = nextHandle_++; - session->SetActiveCommandFailureCallback([this, handle](int transportStatus, uint8_t sbpStatus) { - IOLockGuard cbLock(lock_); - auto* rec = FindByHandle(handle); - if (rec == nullptr) { - return; - } - - FailActiveCommandLocked(*rec, transportStatus, sbpStatus, false); - }); SBP2SessionRecord record{}; record.handle = handle; @@ -579,16 +570,14 @@ void SBP2SessionRegistry::OnBusReset(uint16_t newGeneration) { if (record.commandInFlight || record.commandORB) { FailActiveCommandLocked(record, static_cast(kIOReturnAborted), - Wire::SBPStatus::kRequestAborted, - true); + Wire::SBPStatus::kRequestAborted); } } } void SBP2SessionRegistry::FailActiveCommandLocked(SBP2SessionRecord& record, int transportStatus, - uint8_t sbpStatus, - bool keepSessionTracking) noexcept { + uint8_t sbpStatus) noexcept { if (!record.commandInFlight) { return; } @@ -616,9 +605,6 @@ void SBP2SessionRegistry::FailActiveCommandLocked(SBP2SessionRecord& record, if (record.commandORB) { record.commandORB->SetAppended(false); } - if (!keepSessionTracking) { - return; - } CleanupCommandResources(record); } diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp index 12fc7a52..7742ecd8 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp @@ -120,8 +120,7 @@ class SBP2SessionRegistry { const SBP2SessionRecord* FindByHandle(uint64_t handle) const; void FailActiveCommandLocked(SBP2SessionRecord& record, int transportStatus, - uint8_t sbpStatus, - bool keepSessionTracking) noexcept; + uint8_t sbpStatus) noexcept; std::shared_ptr ResolveUnit(uint64_t guid, uint32_t romOffset) const; diff --git a/tests/SBP2SessionRegistryTests.cpp b/tests/SBP2SessionRegistryTests.cpp index 5d10296c..51b6bcf3 100644 --- a/tests/SBP2SessionRegistryTests.cpp +++ b/tests/SBP2SessionRegistryTests.cpp @@ -301,6 +301,7 @@ TEST(SBP2SessionRegistryTests, ActiveCommandFailsOnceAfterFetchAgentRetryExhaust ASSERT_TRUE(rig.registry.SubmitCommand(handle, request)); const auto fetchAgentWrite = rig.bus.WriteAt(rig.bus.WriteCount() - 1); ASSERT_EQ(8u, fetchAgentWrite.data.size()); + const uint64_t commandOrbAddress = DecodeAddressFromWritePayload(fetchAgentWrite.data); ASSERT_TRUE(rig.bus.CompleteWrite(fetchAgentWrite.handle, ASFW::Async::AsyncStatus::kTimeout)); rig.AdvanceMs(1000); @@ -323,6 +324,10 @@ TEST(SBP2SessionRegistryTests, ActiveCommandFailsOnceAfterFetchAgentRetryExhaust ASSERT_TRUE(rig.bus.CompleteWrite(agentResetWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); while (rig.queue.DrainReadyForTesting() > 0U) { } + + uint32_t ignored = 0; + EXPECT_EQ(ASFW::Async::ResponseCode::AddressError, + rig.addressManager.ReadQuadlet(commandOrbAddress, &ignored)); } TEST(SBP2SessionRegistryTests, SubmitTaskManagementWritesLogicalUnitResetORB) { From d93376b9034bf0331c60b4221f82c5f7445d19d3 Mon Sep 17 00:00:00 2001 From: gly11 Date: Thu, 28 May 2026 15:05:21 +0800 Subject: [PATCH 10/22] fix(sbp2): preserve inquiry failure status --- .../Protocols/SBP2/SBP2SessionRegistry.cpp | 14 +++------- .../Protocols/SBP2/SBP2SessionRegistry.hpp | 2 +- .../UserClient/Handlers/SBP2Handler.hpp | 11 +++++++- tests/SBP2SessionRegistryTests.cpp | 26 +++++++++++++++++++ 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp index f08f45b7..53d3ad00 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp @@ -212,7 +212,7 @@ bool SBP2SessionRegistry::SubmitInquiry(uint64_t handle, uint8_t allocationLengt return SubmitCommand(handle, SCSI::BuildInquiryRequest(allocationLength)); } -std::optional> SBP2SessionRegistry::GetInquiryResult(uint64_t handle) { +std::optional SBP2SessionRegistry::GetInquiryResult(uint64_t handle) { IOLockGuard lock(lock_); auto* record = FindByHandle(handle); if (!record || !record->commandReady || !record->pendingCommandResult.has_value() || @@ -220,19 +220,11 @@ std::optional> SBP2SessionRegistry::GetInquiryResult(uint64 return std::nullopt; } - if (record->pendingCommandResult->transportStatus != 0 || - record->pendingCommandResult->sbpStatus != Wire::SBPStatus::kNoAdditionalInfo) { - record->pendingCommandResult.reset(); - record->lastCompletedCommandOpcode.reset(); - record->commandReady = false; - return std::nullopt; - } - - auto payload = std::move(record->pendingCommandResult->payload); + SCSI::CommandResult result = std::move(*record->pendingCommandResult); record->pendingCommandResult.reset(); record->lastCompletedCommandOpcode.reset(); record->commandReady = false; - return payload; + return result; } bool SBP2SessionRegistry::SubmitCommand(uint64_t handle, const SCSI::CommandRequest& request) { diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp index 7742ecd8..05b9fdc4 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp @@ -86,7 +86,7 @@ class SBP2SessionRegistry { [[nodiscard]] bool SubmitInquiry(uint64_t handle, uint8_t allocationLength = 96); // Get inquiry result (destructive read). Returns nullopt if not ready. - [[nodiscard]] std::optional> GetInquiryResult(uint64_t handle); + [[nodiscard]] std::optional GetInquiryResult(uint64_t handle); // Submit a generic SCSI command. Returns false if not logged in or another command is active. [[nodiscard]] bool SubmitCommand(uint64_t handle, const SCSI::CommandRequest& request); diff --git a/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp b/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp index e76311b1..d4169bea 100644 --- a/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp +++ b/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp @@ -249,8 +249,17 @@ class SBP2Handler { if (!result.has_value()) { return kIOReturnNotFound; } + if (result->transportStatus != 0) { + return result->transportStatus > 0 + ? static_cast(result->transportStatus) + : kIOReturnError; + } + if (result->sbpStatus != ASFW::Protocols::SBP2::Wire::SBPStatus::kNoAdditionalInfo) { + return kIOReturnError; + } - OSData* output = OSData::withBytes(result->data(), static_cast(result->size())); + OSData* output = OSData::withBytes(result->payload.data(), + static_cast(result->payload.size())); if (!output) { return kIOReturnNoMemory; } diff --git a/tests/SBP2SessionRegistryTests.cpp b/tests/SBP2SessionRegistryTests.cpp index 51b6bcf3..a5c7eb2d 100644 --- a/tests/SBP2SessionRegistryTests.cpp +++ b/tests/SBP2SessionRegistryTests.cpp @@ -288,6 +288,32 @@ TEST(SBP2SessionRegistryTests, SubmitRequestSenseCapturesPayloadAndSenseData) { EXPECT_EQ(sensePayload, result->senseData); } +TEST(SBP2SessionRegistryTests, InquiryFailureResultPreservesSBPStatus) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + ASSERT_TRUE(rig.registry.SubmitInquiry(handle, 36)); + const auto& write = rig.bus.WriteAt(rig.bus.WriteCount() - 1); + const uint64_t commandOrbAddress = DecodeAddressFromWritePayload(write.data); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kRequestAborted; + status.orbOffsetHi = ToBE16(static_cast((commandOrbAddress >> 32) & 0xFFFFu)); + status.orbOffsetLo = ToBE32(static_cast(commandOrbAddress & 0xFFFF'FFFFu)); + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + auto result = rig.registry.GetInquiryResult(handle); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(0, result->transportStatus); + EXPECT_EQ(SBPStatus::kRequestAborted, result->sbpStatus); + EXPECT_TRUE(result->payload.empty()); + EXPECT_FALSE(rig.registry.GetInquiryResult(handle).has_value()); +} + TEST(SBP2SessionRegistryTests, ActiveCommandFailsOnceAfterFetchAgentRetryExhaustion) { SessionRegistryRig rig; const uint64_t handle = rig.CreateSession(); From 7f9dc8bf151018698dc180564654db0dff8162e0 Mon Sep 17 00:00:00 2001 From: gly11 Date: Thu, 28 May 2026 15:05:41 +0800 Subject: [PATCH 11/22] fix(sbp2): reject duplicate target sessions --- .../Protocols/SBP2/SBP2SessionRegistry.cpp | 54 +++++++++++++++---- .../Protocols/SBP2/SBP2SessionRegistry.hpp | 10 +++- tests/SBP2SessionRegistryTests.cpp | 42 +++++++++++++++ 3 files changed, 94 insertions(+), 12 deletions(-) diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp index 53d3ad00..3cf77d58 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp @@ -137,11 +137,18 @@ std::expected SBP2SessionRegistry::CreateSession(void* owner, return std::unexpected(kIOReturnUnsupported); } + IOLockGuard lock(lock_); + if (HasSessionForTargetLocked(guid, romOffset)) { + ASFW_LOG(SBP2, + "SBP2SessionRegistry: duplicate session for guid=0x%016llx romOffset=%u", + guid, romOffset); + return std::unexpected(kIOReturnExclusiveAccess); + } + auto session = std::make_shared(bus_, busInfo_, addrSpaceMgr_); session->Configure(targetInfo); session->SetWorkQueue(workQueue_); - IOLockGuard lock(lock_); const uint64_t handle = nextHandle_++; SBP2SessionRecord record{}; @@ -479,7 +486,7 @@ bool SBP2SessionRegistry::ReleaseSession(uint64_t handle) { } } else if (record.session && record.session->State() == LoginState::LoggingOut) { SetReleaseLogoutCallbackLocked(handle, record.session); - RetireSessionLocked(record.session); + RetireSessionLocked(record); sessions_.erase(it); return true; } else { @@ -512,7 +519,7 @@ bool SBP2SessionRegistry::ReleaseSession(uint64_t handle) { CleanupCommandResources(it->second); CleanupManagementResources(it->second); if (it->second.session && it->second.session->State() == LoginState::LoggingOut) { - RetireSessionLocked(it->second.session); + RetireSessionLocked(it->second); } sessions_.erase(it); } @@ -531,7 +538,7 @@ void SBP2SessionRegistry::ReleaseOwner(void* owner) { if (record.session->Logout()) { CleanupCommandResources(record); CleanupManagementResources(record); - RetireSessionLocked(record.session); + RetireSessionLocked(record); it = sessions_.erase(it); continue; } @@ -539,7 +546,7 @@ void SBP2SessionRegistry::ReleaseOwner(void* owner) { SetReleaseLogoutCallbackLocked(record.handle, record.session); CleanupCommandResources(record); CleanupManagementResources(record); - RetireSessionLocked(record.session); + RetireSessionLocked(record); it = sessions_.erase(it); continue; } @@ -687,14 +694,38 @@ void SBP2SessionRegistry::CleanupManagementResources(SBP2SessionRecord& record) record.managementORB.reset(); } -void SBP2SessionRegistry::RetireSessionLocked(std::shared_ptr session) { - if (session == nullptr) { +bool SBP2SessionRegistry::HasSessionForTargetLocked(uint64_t guid, uint32_t romOffset) const { + for (const auto& [handle, record] : sessions_) { + if (record.guid == guid && record.romOffset == romOffset && record.session != nullptr) { + return true; + } + } + + return std::any_of(retiringSessions_.begin(), retiringSessions_.end(), + [guid, romOffset](const RetiringSession& retired) { + return retired.guid == guid && + retired.romOffset == romOffset && + retired.session != nullptr; + }); +} + +void SBP2SessionRegistry::RetireSessionLocked(const SBP2SessionRecord& record) { + if (record.session == nullptr) { return; } - if (std::find(retiringSessions_.begin(), retiringSessions_.end(), session) == + const auto it = std::find_if( + retiringSessions_.begin(), retiringSessions_.end(), + [&record](const RetiringSession& retired) { + return retired.session == record.session; + }); + if (it == retiringSessions_.end()) { - retiringSessions_.push_back(std::move(session)); + retiringSessions_.push_back(RetiringSession{ + .guid = record.guid, + .romOffset = record.romOffset, + .session = record.session, + }); } } @@ -705,7 +736,10 @@ void SBP2SessionRegistry::EraseRetiredSessionLocked( } retiringSessions_.erase( - std::remove(retiringSessions_.begin(), retiringSessions_.end(), session), + std::remove_if(retiringSessions_.begin(), retiringSessions_.end(), + [&session](const RetiringSession& retired) { + return retired.session == session; + }), retiringSessions_.end()); } diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp index 05b9fdc4..23b37a42 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp @@ -126,7 +126,8 @@ class SBP2SessionRegistry { void CleanupCommandResources(SBP2SessionRecord& record); void CleanupManagementResources(SBP2SessionRecord& record); - void RetireSessionLocked(std::shared_ptr session); + [[nodiscard]] bool HasSessionForTargetLocked(uint64_t guid, uint32_t romOffset) const; + void RetireSessionLocked(const SBP2SessionRecord& record); void EraseRetiredSessionLocked(const std::shared_ptr& session); void SetReleaseLogoutCallbackLocked(uint64_t handle, const std::shared_ptr& session); @@ -141,8 +142,13 @@ class SBP2SessionRegistry { IOLock* lock_{nullptr}; std::map sessions_; + struct RetiringSession { + uint64_t guid{0}; + uint32_t romOffset{0}; + std::shared_ptr session; + }; // Hidden from registry clients, but retained until async logout finishes or times out. - std::vector> retiringSessions_; + std::vector retiringSessions_; uint64_t nextHandle_{1}; }; diff --git a/tests/SBP2SessionRegistryTests.cpp b/tests/SBP2SessionRegistryTests.cpp index a5c7eb2d..cadc2e9e 100644 --- a/tests/SBP2SessionRegistryTests.cpp +++ b/tests/SBP2SessionRegistryTests.cpp @@ -548,6 +548,48 @@ TEST(SBP2SessionRegistryTests, ReleaseOwnerRetainsSessionUntilLogoutTimeout) { EXPECT_FALSE(rig.registry.GetSessionState(handle).has_value()); } +TEST(SBP2SessionRegistryTests, CreateSessionRejectsDuplicateTargetAcrossOwners) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + ASSERT_NE(0u, handle); + + auto duplicate = rig.registry.CreateSession(reinterpret_cast(0xBEEF), + SessionRegistryRig::kGuid, + 0); + ASSERT_FALSE(duplicate.has_value()); + EXPECT_EQ(kIOReturnExclusiveAccess, duplicate.error()); +} + +TEST(SBP2SessionRegistryTests, CreateSessionRejectsDuplicateTargetUntilLogoutCompletes) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + const size_t writesBeforeRelease = rig.bus.WriteCount(); + rig.registry.ReleaseOwner(reinterpret_cast(0xCAFE)); + + auto duplicateWhileLoggingOut = rig.registry.CreateSession(reinterpret_cast(0xBEEF), + SessionRegistryRig::kGuid, + 0); + ASSERT_FALSE(duplicateWhileLoggingOut.has_value()); + EXPECT_EQ(kIOReturnExclusiveAccess, duplicateWhileLoggingOut.error()); + + const auto& logoutWrite = rig.bus.WriteAt(writesBeforeRelease); + ASSERT_TRUE(rig.bus.CompleteWrite(logoutWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + auto replacement = rig.registry.CreateSession(reinterpret_cast(0xBEEF), + SessionRegistryRig::kGuid, + 0); + ASSERT_TRUE(replacement.has_value()); +} + TEST(SBP2SessionRegistryTests, MissingDiscoveryStillTerminatesNonSBP2Device) { DeviceManager deviceManager; From 9bb9464d60e714883a09d154e32441eb1740e1f4 Mon Sep 17 00:00:00 2001 From: gly11 Date: Sun, 31 May 2026 17:14:18 +0800 Subject: [PATCH 12/22] test(sbp2): cover registry target info and release edges --- tests/SBP2SessionRegistryTests.cpp | 70 ++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/SBP2SessionRegistryTests.cpp b/tests/SBP2SessionRegistryTests.cpp index cadc2e9e..7a51d7db 100644 --- a/tests/SBP2SessionRegistryTests.cpp +++ b/tests/SBP2SessionRegistryTests.cpp @@ -548,6 +548,63 @@ TEST(SBP2SessionRegistryTests, ReleaseOwnerRetainsSessionUntilLogoutTimeout) { EXPECT_FALSE(rig.registry.GetSessionState(handle).has_value()); } +TEST(SBP2SessionRegistryTests, ReleaseOwnerDuringPendingLoginCancelsWriteAndDropsSession) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + + std::weak_ptr weakSession = + rig.registry.GetSessionWeakForTesting(handle); + ASSERT_FALSE(weakSession.expired()); + + ASSERT_TRUE(rig.registry.StartLogin(handle)); + ASSERT_EQ(1u, rig.bus.PendingWriteCount()); + const auto loginWrite = rig.bus.WriteAt(0); + + rig.registry.ReleaseOwner(reinterpret_cast(0xCAFE)); + + EXPECT_TRUE(weakSession.expired()); + EXPECT_FALSE(rig.registry.GetSessionState(handle).has_value()); + EXPECT_EQ(0u, rig.bus.PendingWriteCount()); + EXPECT_FALSE(rig.bus.CompleteWrite(loginWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); +} + +TEST(SBP2SessionRegistryTests, ReleaseSessionDuringPendingLogoutRetainsUntilStatusArrives) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + auto* session = rig.registry.GetSessionForTesting(handle); + ASSERT_NE(nullptr, session); + + std::weak_ptr weakSession = + rig.registry.GetSessionWeakForTesting(handle); + ASSERT_FALSE(weakSession.expired()); + + const size_t writesBeforeLogout = rig.bus.WriteCount(); + const size_t pendingBeforeLogout = rig.bus.PendingWriteCount(); + ASSERT_TRUE(session->Logout()); + ASSERT_EQ(writesBeforeLogout + 1U, rig.bus.WriteCount()); + ASSERT_EQ(pendingBeforeLogout + 1U, rig.bus.PendingWriteCount()); + + EXPECT_TRUE(rig.registry.ReleaseSession(handle)); + EXPECT_FALSE(weakSession.expired()); + EXPECT_FALSE(rig.registry.GetSessionState(handle).has_value()); + + const auto& logoutWrite = rig.bus.WriteAt(writesBeforeLogout); + EXPECT_TRUE(rig.bus.CompleteWrite(logoutWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); + EXPECT_FALSE(weakSession.expired()); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + EXPECT_TRUE(weakSession.expired()); + EXPECT_FALSE(rig.registry.GetSessionState(handle).has_value()); +} + TEST(SBP2SessionRegistryTests, CreateSessionRejectsDuplicateTargetAcrossOwners) { SessionRegistryRig rig; const uint64_t handle = rig.CreateSession(); @@ -640,6 +697,19 @@ TEST(SBP2SessionRegistryTests, CreateSessionAcceptsRealSBP2SpecAndVersion) { ASSERT_TRUE(result.has_value()); } +TEST(SBP2SessionRegistryTests, UnitCharacteristicsDecodeMinimumORBSize) { + SessionRegistryRig rig(0x000408); + + const uint64_t handle = rig.CreateSession(); + auto* session = rig.registry.GetSessionForTesting(handle); + ASSERT_NE(nullptr, session); + + const auto& targetInfo = session->TargetInfo(); + EXPECT_EQ(2000u, targetInfo.managementTimeoutMs); + EXPECT_EQ(32u, targetInfo.maxORBSize); + EXPECT_EQ(12u, targetInfo.maxCommandBlockSize); +} + TEST(SBP2SessionRegistryTests, UnitCharacteristicsDecodeTimeoutAndORBSizeFromLowBytes) { SessionRegistryRig rig(0x000410); From 0213018a0742b2954cae19e42725b487b4178355 Mon Sep 17 00:00:00 2001 From: gly11 Date: Sun, 31 May 2026 17:24:07 +0800 Subject: [PATCH 13/22] fix(sbp2): validate session owner for client calls --- .../Protocols/SBP2/SBP2SessionRegistry.cpp | 51 ++++--- .../Protocols/SBP2/SBP2SessionRegistry.hpp | 26 ++-- .../UserClient/Core/ASFWDriverUserClient.cpp | 14 +- .../UserClient/Handlers/SBP2Handler.hpp | 34 +++-- tests/SBP2SessionRegistryTests.cpp | 132 +++++++++++++----- 5 files changed, 173 insertions(+), 84 deletions(-) diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp index 3cf77d58..d8201f06 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp @@ -168,9 +168,9 @@ std::expected SBP2SessionRegistry::CreateSession(void* owner, return handle; } -bool SBP2SessionRegistry::StartLogin(uint64_t handle) { +bool SBP2SessionRegistry::StartLogin(void* owner, uint64_t handle) { IOLockGuard lock(lock_); - auto* record = FindByHandle(handle); + auto* record = FindByHandleForOwner(owner, handle); if (!record || !record->session) { return false; } @@ -199,9 +199,10 @@ bool SBP2SessionRegistry::StartLogin(uint64_t handle) { return record->session->Login(); } -std::optional SBP2SessionRegistry::GetSessionState(uint64_t handle) const { +std::optional SBP2SessionRegistry::GetSessionState(void* owner, + uint64_t handle) const { IOLockGuard lock(lock_); - const auto* record = FindByHandle(handle); + const auto* record = FindByHandleForOwner(owner, handle); if (!record || !record->session) { return std::nullopt; } @@ -215,13 +216,14 @@ std::optional SBP2SessionRegistry::GetSessionState(uint64_t ha return state; } -bool SBP2SessionRegistry::SubmitInquiry(uint64_t handle, uint8_t allocationLength) { - return SubmitCommand(handle, SCSI::BuildInquiryRequest(allocationLength)); +bool SBP2SessionRegistry::SubmitInquiry(void* owner, uint64_t handle, uint8_t allocationLength) { + return SubmitCommand(owner, handle, SCSI::BuildInquiryRequest(allocationLength)); } -std::optional SBP2SessionRegistry::GetInquiryResult(uint64_t handle) { +std::optional SBP2SessionRegistry::GetInquiryResult(void* owner, + uint64_t handle) { IOLockGuard lock(lock_); - auto* record = FindByHandle(handle); + auto* record = FindByHandleForOwner(owner, handle); if (!record || !record->commandReady || !record->pendingCommandResult.has_value() || record->lastCompletedCommandOpcode != kInquiryOpcode) { return std::nullopt; @@ -234,13 +236,15 @@ std::optional SBP2SessionRegistry::GetInquiryResult(uint64_ return result; } -bool SBP2SessionRegistry::SubmitCommand(uint64_t handle, const SCSI::CommandRequest& request) { +bool SBP2SessionRegistry::SubmitCommand(void* owner, + uint64_t handle, + const SCSI::CommandRequest& request) { std::shared_ptr session; SBP2CommandORB* submittedORB = nullptr; { IOLockGuard lock(lock_); - auto* record = FindByHandle(handle); + auto* record = FindByHandleForOwner(owner, handle); if (!record || !record->session || request.cdb.empty()) { return false; } @@ -384,7 +388,7 @@ bool SBP2SessionRegistry::SubmitCommand(uint64_t handle, const SCSI::CommandRequ } IOLockGuard lock(lock_); - auto* record = FindByHandle(handle); + auto* record = FindByHandleForOwner(owner, handle); if (record != nullptr && record->commandORB.get() == submittedORB) { record->commandInFlight = false; record->commandReady = false; @@ -396,9 +400,10 @@ bool SBP2SessionRegistry::SubmitCommand(uint64_t handle, const SCSI::CommandRequ return false; } -std::optional SBP2SessionRegistry::GetCommandResult(uint64_t handle) { +std::optional SBP2SessionRegistry::GetCommandResult(void* owner, + uint64_t handle) { IOLockGuard lock(lock_); - auto* record = FindByHandle(handle); + auto* record = FindByHandleForOwner(owner, handle); if (!record || !record->commandReady || !record->pendingCommandResult.has_value()) { return std::nullopt; } @@ -410,10 +415,11 @@ std::optional SBP2SessionRegistry::GetCommandResult(uint64_ return result; } -bool SBP2SessionRegistry::SubmitTaskManagement(uint64_t handle, +bool SBP2SessionRegistry::SubmitTaskManagement(void* owner, + uint64_t handle, SBP2ManagementORB::Function function) { IOLockGuard lock(lock_); - auto* record = FindByHandle(handle); + auto* record = FindByHandleForOwner(owner, handle); if (!record || !record->session || !IsSupportedTaskManagementFunction(function)) { return false; } @@ -461,13 +467,13 @@ bool SBP2SessionRegistry::SubmitTaskManagement(uint64_t handle, return true; } -bool SBP2SessionRegistry::ReleaseSession(uint64_t handle) { +bool SBP2SessionRegistry::ReleaseSession(void* owner, uint64_t handle) { uint32_t waitMs = 0; { IOLockGuard lock(lock_); auto it = sessions_.find(handle); - if (it == sessions_.end()) { + if (it == sessions_.end() || it->second.owner != owner) { return false; } @@ -661,6 +667,17 @@ const SBP2SessionRecord* SBP2SessionRegistry::FindByHandle(uint64_t handle) cons return it != sessions_.end() ? &it->second : nullptr; } +SBP2SessionRecord* SBP2SessionRegistry::FindByHandleForOwner(void* owner, uint64_t handle) { + auto* record = FindByHandle(handle); + return (record != nullptr && record->owner == owner) ? record : nullptr; +} + +const SBP2SessionRecord* SBP2SessionRegistry::FindByHandleForOwner(void* owner, + uint64_t handle) const { + const auto* record = FindByHandle(handle); + return (record != nullptr && record->owner == owner) ? record : nullptr; +} + std::shared_ptr SBP2SessionRegistry::ResolveUnit(uint64_t guid, uint32_t romOffset) const { const auto devices = deviceManager_.GetAllDevices(); diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp index 23b37a42..b1ec3ca4 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp @@ -77,29 +77,37 @@ class SBP2SessionRegistry { uint32_t romOffset); // Start login for a session. Returns false if not in Idle state. - [[nodiscard]] bool StartLogin(uint64_t handle); + [[nodiscard]] bool StartLogin(void* owner, uint64_t handle); // Get session state. Returns nullopt if handle not found. - [[nodiscard]] std::optional GetSessionState(uint64_t handle) const; + [[nodiscard]] std::optional GetSessionState(void* owner, + uint64_t handle) const; // Submit SCSI INQUIRY. Returns false if not logged in or inquiry already in-flight. - [[nodiscard]] bool SubmitInquiry(uint64_t handle, uint8_t allocationLength = 96); + [[nodiscard]] bool SubmitInquiry(void* owner, + uint64_t handle, + uint8_t allocationLength = 96); // Get inquiry result (destructive read). Returns nullopt if not ready. - [[nodiscard]] std::optional GetInquiryResult(uint64_t handle); + [[nodiscard]] std::optional GetInquiryResult(void* owner, + uint64_t handle); // Submit a generic SCSI command. Returns false if not logged in or another command is active. - [[nodiscard]] bool SubmitCommand(uint64_t handle, const SCSI::CommandRequest& request); + [[nodiscard]] bool SubmitCommand(void* owner, + uint64_t handle, + const SCSI::CommandRequest& request); // Get generic command result (destructive read). Returns nullopt if not ready. - [[nodiscard]] std::optional GetCommandResult(uint64_t handle); + [[nodiscard]] std::optional GetCommandResult(void* owner, + uint64_t handle); // Submit a task-management recovery ORB. - [[nodiscard]] bool SubmitTaskManagement(uint64_t handle, + [[nodiscard]] bool SubmitTaskManagement(void* owner, + uint64_t handle, SBP2ManagementORB::Function function); // Release a specific session. - [[nodiscard]] bool ReleaseSession(uint64_t handle); + [[nodiscard]] bool ReleaseSession(void* owner, uint64_t handle); // Release all sessions for an owner (best-effort logout + cleanup). void ReleaseOwner(void* owner); @@ -118,6 +126,8 @@ class SBP2SessionRegistry { private: SBP2SessionRecord* FindByHandle(uint64_t handle); const SBP2SessionRecord* FindByHandle(uint64_t handle) const; + SBP2SessionRecord* FindByHandleForOwner(void* owner, uint64_t handle); + const SBP2SessionRecord* FindByHandleForOwner(void* owner, uint64_t handle) const; void FailActiveCommandLocked(SBP2SessionRecord& record, int transportStatus, uint8_t sbpStatus) noexcept; diff --git a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp index ad4d4a83..8b6e9c14 100644 --- a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp +++ b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp @@ -511,28 +511,28 @@ kern_return_t ASFWDriverUserClient::ExternalMethod(uint64_t selector, return runtimeState->SBP2().CreateSBP2Session(arguments, this); case kMethodStartSBP2Login: - return runtimeState->SBP2().StartSBP2Login(arguments); + return runtimeState->SBP2().StartSBP2Login(arguments, this); case kMethodGetSBP2SessionState: - return runtimeState->SBP2().GetSBP2SessionState(arguments); + return runtimeState->SBP2().GetSBP2SessionState(arguments, this); case kMethodSubmitSBP2Inquiry: - return runtimeState->SBP2().SubmitSBP2Inquiry(arguments); + return runtimeState->SBP2().SubmitSBP2Inquiry(arguments, this); case kMethodGetSBP2InquiryResult: - return runtimeState->SBP2().GetSBP2InquiryResult(arguments); + return runtimeState->SBP2().GetSBP2InquiryResult(arguments, this); case kMethodReleaseSBP2Session: return runtimeState->SBP2().ReleaseSBP2Session(arguments, this); case kMethodSubmitSBP2Command: - return runtimeState->SBP2().SubmitSBP2Command(arguments); + return runtimeState->SBP2().SubmitSBP2Command(arguments, this); case kMethodGetSBP2CommandResult: - return runtimeState->SBP2().GetSBP2CommandResult(arguments); + return runtimeState->SBP2().GetSBP2CommandResult(arguments, this); case kMethodSubmitSBP2TaskManagement: - return runtimeState->SBP2().SubmitSBP2TaskManagement(arguments); + return runtimeState->SBP2().SubmitSBP2TaskManagement(arguments, this); default: return kIOReturnBadArgument; diff --git a/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp b/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp index d4169bea..f6ebce6c 100644 --- a/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp +++ b/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp @@ -185,7 +185,7 @@ class SBP2Handler { return kIOReturnSuccess; } - kern_return_t StartSBP2Login(IOUserClientMethodArguments* args) { + kern_return_t StartSBP2Login(IOUserClientMethodArguments* args, void* owner) { if (!registry_) { return kIOReturnNotReady; } @@ -194,10 +194,10 @@ class SBP2Handler { } const uint64_t handle = args->scalarInput[0]; - return registry_->StartLogin(handle) ? kIOReturnSuccess : kIOReturnError; + return registry_->StartLogin(owner, handle) ? kIOReturnSuccess : kIOReturnError; } - kern_return_t GetSBP2SessionState(IOUserClientMethodArguments* args) { + kern_return_t GetSBP2SessionState(IOUserClientMethodArguments* args, void* owner) { if (!registry_) { return kIOReturnNotReady; } @@ -206,7 +206,7 @@ class SBP2Handler { } const uint64_t handle = args->scalarInput[0]; - auto state = registry_->GetSessionState(handle); + auto state = registry_->GetSessionState(owner, handle); if (!state.has_value()) { return kIOReturnNotFound; } @@ -223,7 +223,7 @@ class SBP2Handler { return kIOReturnSuccess; } - kern_return_t SubmitSBP2Inquiry(IOUserClientMethodArguments* args) { + kern_return_t SubmitSBP2Inquiry(IOUserClientMethodArguments* args, void* owner) { if (!registry_) { return kIOReturnNotReady; } @@ -233,10 +233,12 @@ class SBP2Handler { const uint64_t handle = args->scalarInput[0]; const uint8_t allocationLength = static_cast(args->scalarInput[1] & 0xFFu); - return registry_->SubmitInquiry(handle, allocationLength) ? kIOReturnSuccess : kIOReturnError; + return registry_->SubmitInquiry(owner, handle, allocationLength) + ? kIOReturnSuccess + : kIOReturnError; } - kern_return_t GetSBP2InquiryResult(IOUserClientMethodArguments* args) { + kern_return_t GetSBP2InquiryResult(IOUserClientMethodArguments* args, void* owner) { if (!registry_) { return kIOReturnNotReady; } @@ -245,7 +247,7 @@ class SBP2Handler { } const uint64_t handle = args->scalarInput[0]; - auto result = registry_->GetInquiryResult(handle); + auto result = registry_->GetInquiryResult(owner, handle); if (!result.has_value()) { return kIOReturnNotFound; } @@ -269,7 +271,7 @@ class SBP2Handler { return kIOReturnSuccess; } - kern_return_t SubmitSBP2Command(IOUserClientMethodArguments* args) { + kern_return_t SubmitSBP2Command(IOUserClientMethodArguments* args, void* owner) { if (!registry_) { return kIOReturnNotReady; } @@ -324,10 +326,10 @@ class SBP2Handler { request.outgoingPayload.assign(cursor, cursor + header->outgoingLength); const uint64_t handle = args->scalarInput[0]; - return registry_->SubmitCommand(handle, request) ? kIOReturnSuccess : kIOReturnError; + return registry_->SubmitCommand(owner, handle, request) ? kIOReturnSuccess : kIOReturnError; } - kern_return_t GetSBP2CommandResult(IOUserClientMethodArguments* args) { + kern_return_t GetSBP2CommandResult(IOUserClientMethodArguments* args, void* owner) { if (!registry_) { return kIOReturnNotReady; } @@ -336,7 +338,7 @@ class SBP2Handler { } const uint64_t handle = args->scalarInput[0]; - auto result = registry_->GetCommandResult(handle); + auto result = registry_->GetCommandResult(owner, handle); if (!result.has_value()) { return kIOReturnNotFound; } @@ -372,7 +374,7 @@ class SBP2Handler { return kIOReturnSuccess; } - kern_return_t SubmitSBP2TaskManagement(IOUserClientMethodArguments* args) { + kern_return_t SubmitSBP2TaskManagement(IOUserClientMethodArguments* args, void* owner) { if (!registry_) { return kIOReturnNotReady; } @@ -396,7 +398,9 @@ class SBP2Handler { } const uint64_t handle = args->scalarInput[0]; - return registry_->SubmitTaskManagement(handle, function) ? kIOReturnSuccess : kIOReturnError; + return registry_->SubmitTaskManagement(owner, handle, function) + ? kIOReturnSuccess + : kIOReturnError; } kern_return_t ReleaseSBP2Session(IOUserClientMethodArguments* args, void* owner) { @@ -408,7 +412,7 @@ class SBP2Handler { } const uint64_t handle = args->scalarInput[0]; - return registry_->ReleaseSession(handle) ? kIOReturnSuccess : kIOReturnNotFound; + return registry_->ReleaseSession(owner, handle) ? kIOReturnSuccess : kIOReturnNotFound; } private: diff --git a/tests/SBP2SessionRegistryTests.cpp b/tests/SBP2SessionRegistryTests.cpp index 7a51d7db..2eda1e2c 100644 --- a/tests/SBP2SessionRegistryTests.cpp +++ b/tests/SBP2SessionRegistryTests.cpp @@ -172,7 +172,7 @@ class SessionRegistryRig { } uint64_t CreateSession() { - auto result = registry.CreateSession(reinterpret_cast(0xCAFE), kGuid, 0); + auto result = registry.CreateSession(Owner(), kGuid, 0); EXPECT_TRUE(result.has_value()); const uint64_t handle = result.value_or(0); if (auto* session = registry.GetSessionForTesting(handle)) { @@ -184,7 +184,7 @@ class SessionRegistryRig { void LoginSuccessfully(uint64_t handle, uint16_t loginId = 0x0042, uint32_t commandBlockAgentLo = 0x0020'0000) { - ASSERT_TRUE(registry.StartLogin(handle)); + ASSERT_TRUE(registry.StartLogin(Owner(), handle)); ASSERT_EQ(1u, bus.PendingWriteCount()); const auto& loginWrite = bus.WriteAt(0); @@ -222,6 +222,8 @@ class SessionRegistryRig { } static constexpr uint64_t kGuid = 0x0003DB0001DDDDA1ULL; + static void* Owner() noexcept { return reinterpret_cast(0xCAFE); } + static void* OtherOwner() noexcept { return reinterpret_cast(0xBEEF); } ASFW::Async::Testing::DeferredFireWireBus bus; AddressSpaceManager addressManager{nullptr}; @@ -256,7 +258,7 @@ TEST(SBP2SessionRegistryTests, SubmitRequestSenseCapturesPayloadAndSenseData) { const auto request = SCSI::BuildRequestSenseRequest(18); const size_t pendingBeforeSubmit = rig.bus.PendingWriteCount(); - ASSERT_TRUE(rig.registry.SubmitCommand(handle, request)); + ASSERT_TRUE(rig.registry.SubmitCommand(SessionRegistryRig::Owner(), handle, request)); ASSERT_EQ(pendingBeforeSubmit + 1U, rig.bus.PendingWriteCount()); const auto& write = rig.bus.WriteAt(rig.bus.WriteCount() - 1); @@ -280,7 +282,7 @@ TEST(SBP2SessionRegistryTests, SubmitRequestSenseCapturesPayloadAndSenseData) { rig.sessionStatusAddress, std::span{reinterpret_cast(&status), sizeof(status)}); - auto result = rig.registry.GetCommandResult(handle); + auto result = rig.registry.GetCommandResult(SessionRegistryRig::Owner(), handle); ASSERT_TRUE(result.has_value()); EXPECT_EQ(0, result->transportStatus); EXPECT_EQ(SBPStatus::kNoAdditionalInfo, result->sbpStatus); @@ -293,7 +295,7 @@ TEST(SBP2SessionRegistryTests, InquiryFailureResultPreservesSBPStatus) { const uint64_t handle = rig.CreateSession(); rig.LoginSuccessfully(handle); - ASSERT_TRUE(rig.registry.SubmitInquiry(handle, 36)); + ASSERT_TRUE(rig.registry.SubmitInquiry(SessionRegistryRig::Owner(), handle, 36)); const auto& write = rig.bus.WriteAt(rig.bus.WriteCount() - 1); const uint64_t commandOrbAddress = DecodeAddressFromWritePayload(write.data); @@ -306,12 +308,12 @@ TEST(SBP2SessionRegistryTests, InquiryFailureResultPreservesSBPStatus) { rig.sessionStatusAddress, std::span{reinterpret_cast(&status), sizeof(status)}); - auto result = rig.registry.GetInquiryResult(handle); + auto result = rig.registry.GetInquiryResult(SessionRegistryRig::Owner(), handle); ASSERT_TRUE(result.has_value()); EXPECT_EQ(0, result->transportStatus); EXPECT_EQ(SBPStatus::kRequestAborted, result->sbpStatus); EXPECT_TRUE(result->payload.empty()); - EXPECT_FALSE(rig.registry.GetInquiryResult(handle).has_value()); + EXPECT_FALSE(rig.registry.GetInquiryResult(SessionRegistryRig::Owner(), handle).has_value()); } TEST(SBP2SessionRegistryTests, ActiveCommandFailsOnceAfterFetchAgentRetryExhaustion) { @@ -324,7 +326,7 @@ TEST(SBP2SessionRegistryTests, ActiveCommandFailsOnceAfterFetchAgentRetryExhaust session->SetFetchAgentWriteRetriesForTesting(0); const auto request = SCSI::BuildTestUnitReadyRequest(); - ASSERT_TRUE(rig.registry.SubmitCommand(handle, request)); + ASSERT_TRUE(rig.registry.SubmitCommand(SessionRegistryRig::Owner(), handle, request)); const auto fetchAgentWrite = rig.bus.WriteAt(rig.bus.WriteCount() - 1); ASSERT_EQ(8u, fetchAgentWrite.data.size()); const uint64_t commandOrbAddress = DecodeAddressFromWritePayload(fetchAgentWrite.data); @@ -334,12 +336,12 @@ TEST(SBP2SessionRegistryTests, ActiveCommandFailsOnceAfterFetchAgentRetryExhaust while (rig.queue.DrainReadyForTesting() > 0U) { } - const auto commandResult = rig.registry.GetCommandResult(handle); + const auto commandResult = rig.registry.GetCommandResult(SessionRegistryRig::Owner(), handle); ASSERT_TRUE(commandResult.has_value()); EXPECT_EQ(-1, commandResult->transportStatus); EXPECT_EQ(SBPStatus::kUnspecifiedError, commandResult->sbpStatus); - const auto stateAfterFailure = rig.registry.GetSessionState(handle); + const auto stateAfterFailure = rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle); ASSERT_TRUE(stateAfterFailure.has_value()); EXPECT_EQ(-1, stateAfterFailure->lastError); @@ -356,6 +358,60 @@ TEST(SBP2SessionRegistryTests, ActiveCommandFailsOnceAfterFetchAgentRetryExhaust rig.addressManager.ReadQuadlet(commandOrbAddress, &ignored)); } +TEST(SBP2SessionRegistryTests, RejectsSessionOperationsFromNonOwningClient) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + + EXPECT_FALSE(rig.registry.StartLogin(SessionRegistryRig::OtherOwner(), handle)); + EXPECT_EQ(0u, rig.bus.PendingWriteCount()); + + rig.LoginSuccessfully(handle); + + EXPECT_FALSE(rig.registry.GetSessionState(SessionRegistryRig::OtherOwner(), handle).has_value()); + EXPECT_FALSE(rig.registry.SubmitCommand(SessionRegistryRig::OtherOwner(), + handle, + SCSI::BuildTestUnitReadyRequest())); + EXPECT_FALSE(rig.registry.SubmitInquiry(SessionRegistryRig::OtherOwner(), handle, 36)); + EXPECT_FALSE(rig.registry.SubmitTaskManagement( + SessionRegistryRig::OtherOwner(), handle, SBP2ManagementORB::Function::LogicalUnitReset)); + EXPECT_FALSE(rig.registry.ReleaseSession(SessionRegistryRig::OtherOwner(), handle)); + + ASSERT_TRUE(rig.registry.SubmitCommand(SessionRegistryRig::Owner(), + handle, + SCSI::BuildTestUnitReadyRequest())); + const auto& commandWrite = rig.bus.WriteAt(rig.bus.WriteCount() - 1); + const uint64_t commandOrbAddress = DecodeAddressFromWritePayload(commandWrite.data); + StatusBlock commandStatus{}; + commandStatus.details = 0; + commandStatus.sbpStatus = SBPStatus::kNoAdditionalInfo; + commandStatus.orbOffsetHi = ToBE16(static_cast((commandOrbAddress >> 32) & 0xFFFFu)); + commandStatus.orbOffsetLo = ToBE32(static_cast(commandOrbAddress & 0xFFFF'FFFFu)); + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&commandStatus), + sizeof(commandStatus)}); + + EXPECT_FALSE(rig.registry.GetCommandResult(SessionRegistryRig::OtherOwner(), handle).has_value()); + EXPECT_TRUE(rig.registry.GetCommandResult(SessionRegistryRig::Owner(), handle).has_value()); + + ASSERT_TRUE(rig.registry.SubmitInquiry(SessionRegistryRig::Owner(), handle, 36)); + const auto& inquiryWrite = rig.bus.WriteAt(rig.bus.WriteCount() - 1); + const uint64_t inquiryOrbAddress = DecodeAddressFromWritePayload(inquiryWrite.data); + StatusBlock inquiryStatus{}; + inquiryStatus.details = 0; + inquiryStatus.sbpStatus = SBPStatus::kRequestAborted; + inquiryStatus.orbOffsetHi = ToBE16(static_cast((inquiryOrbAddress >> 32) & 0xFFFFu)); + inquiryStatus.orbOffsetLo = ToBE32(static_cast(inquiryOrbAddress & 0xFFFF'FFFFu)); + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&inquiryStatus), + sizeof(inquiryStatus)}); + + EXPECT_FALSE(rig.registry.GetInquiryResult(SessionRegistryRig::OtherOwner(), handle).has_value()); + EXPECT_TRUE(rig.registry.GetInquiryResult(SessionRegistryRig::Owner(), handle).has_value()); + EXPECT_TRUE(rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle).has_value()); +} + TEST(SBP2SessionRegistryTests, SubmitTaskManagementWritesLogicalUnitResetORB) { SessionRegistryRig rig; const uint64_t handle = rig.CreateSession(); @@ -363,7 +419,7 @@ TEST(SBP2SessionRegistryTests, SubmitTaskManagementWritesLogicalUnitResetORB) { const size_t writesBeforeSubmit = rig.bus.WriteCount(); ASSERT_TRUE(rig.registry.SubmitTaskManagement( - handle, SBP2ManagementORB::Function::LogicalUnitReset)); + SessionRegistryRig::Owner(), handle, SBP2ManagementORB::Function::LogicalUnitReset)); ASSERT_EQ(writesBeforeSubmit + 1U, rig.bus.WriteCount()); const auto& write = rig.bus.WriteAt(writesBeforeSubmit); @@ -379,15 +435,15 @@ TEST(SBP2SessionRegistryTests, TaskManagementSuccessClearsActiveCommandTracking) rig.LoginSuccessfully(handle); const auto request = SCSI::BuildTestUnitReadyRequest(); - ASSERT_TRUE(rig.registry.SubmitCommand(handle, request)); - ASSERT_FALSE(rig.registry.GetCommandResult(handle).has_value()); + ASSERT_TRUE(rig.registry.SubmitCommand(SessionRegistryRig::Owner(), handle, request)); + ASSERT_FALSE(rig.registry.GetCommandResult(SessionRegistryRig::Owner(), handle).has_value()); ASSERT_GT(rig.bus.WriteCount(), 0U); const auto fetchAgentWrite = rig.bus.WriteAt(rig.bus.WriteCount() - 1); ASSERT_EQ(8U, fetchAgentWrite.data.size()); const size_t writesBeforeTaskManagement = rig.bus.WriteCount(); ASSERT_TRUE(rig.registry.SubmitTaskManagement( - handle, SBP2ManagementORB::Function::AbortTaskSet)); + SessionRegistryRig::Owner(), handle, SBP2ManagementORB::Function::AbortTaskSet)); const auto& taskWrite = rig.bus.WriteAt(writesBeforeTaskManagement); const uint64_t taskOrbAddress = DecodeAddressFromWritePayload(taskWrite.data); const uint64_t taskStatusAddress = @@ -396,10 +452,10 @@ TEST(SBP2SessionRegistryTests, TaskManagementSuccessClearsActiveCommandTracking) ASSERT_TRUE(rig.bus.CompleteWrite(taskWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); CompleteTaskManagementStatus(rig.addressManager, taskStatusAddress, taskOrbAddress); - EXPECT_FALSE(rig.registry.GetCommandResult(handle).has_value()); + EXPECT_FALSE(rig.registry.GetCommandResult(SessionRegistryRig::Owner(), handle).has_value()); EXPECT_FALSE(rig.bus.CompleteWrite(fetchAgentWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); - EXPECT_TRUE(rig.registry.SubmitCommand(handle, request)); + EXPECT_TRUE(rig.registry.SubmitCommand(SessionRegistryRig::Owner(), handle, request)); } TEST(SBP2SessionRegistryTests, SubmitTaskManagementRejectsInvalidFunction) { @@ -408,7 +464,7 @@ TEST(SBP2SessionRegistryTests, SubmitTaskManagementRejectsInvalidFunction) { rig.LoginSuccessfully(handle); EXPECT_FALSE(rig.registry.SubmitTaskManagement( - handle, SBP2ManagementORB::Function::QueryLogins)); + SessionRegistryRig::Owner(), handle, SBP2ManagementORB::Function::QueryLogins)); } TEST(SBP2SessionRegistryTests, MissingDiscoverySuspendsDeviceAndReconnectWaitsForResume) { @@ -417,7 +473,7 @@ TEST(SBP2SessionRegistryTests, MissingDiscoverySuspendsDeviceAndReconnectWaitsFo rig.LoginSuccessfully(handle); rig.registry.OnBusReset(2); - const auto suspendedState = rig.registry.GetSessionState(handle); + const auto suspendedState = rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle); ASSERT_TRUE(suspendedState.has_value()); EXPECT_EQ(ASFW::Protocols::SBP2::LoginState::Suspended, suspendedState->loginState); @@ -465,7 +521,9 @@ TEST(SBP2SessionRegistryTests, MissingDiscoverySuspendsDeviceAndReconnectWaitsFo EXPECT_EQ(0x33u, busyTimeoutWrite.address.nodeID); const size_t writesBeforeCommand = rig.bus.WriteCount(); - ASSERT_TRUE(rig.registry.SubmitCommand(handle, SCSI::BuildTestUnitReadyRequest())); + ASSERT_TRUE(rig.registry.SubmitCommand(SessionRegistryRig::Owner(), + handle, + SCSI::BuildTestUnitReadyRequest())); ASSERT_EQ(writesBeforeCommand + 1U, rig.bus.WriteCount()); const auto& fetchAgentWrite = rig.bus.WriteAt(writesBeforeCommand); EXPECT_EQ(0x33u, fetchAgentWrite.nodeId.value); @@ -501,11 +559,11 @@ TEST(SBP2SessionRegistryTests, ReleaseOwnerRetainsSessionUntilLogoutStatusArrive ASSERT_FALSE(weakSession.expired()); const size_t writesBeforeRelease = rig.bus.WriteCount(); - rig.registry.ReleaseOwner(reinterpret_cast(0xCAFE)); + rig.registry.ReleaseOwner(SessionRegistryRig::Owner()); EXPECT_FALSE(weakSession.expired()); ASSERT_EQ(writesBeforeRelease + 1U, rig.bus.WriteCount()); - EXPECT_FALSE(rig.registry.GetSessionState(handle).has_value()); + EXPECT_FALSE(rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle).has_value()); const auto& logoutWrite = rig.bus.WriteAt(writesBeforeRelease); EXPECT_TRUE(rig.bus.CompleteWrite(logoutWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); @@ -519,7 +577,7 @@ TEST(SBP2SessionRegistryTests, ReleaseOwnerRetainsSessionUntilLogoutStatusArrive std::span{reinterpret_cast(&status), sizeof(status)}); EXPECT_TRUE(weakSession.expired()); - EXPECT_FALSE(rig.registry.GetSessionState(handle).has_value()); + EXPECT_FALSE(rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle).has_value()); } TEST(SBP2SessionRegistryTests, ReleaseOwnerRetainsSessionUntilLogoutTimeout) { @@ -532,11 +590,11 @@ TEST(SBP2SessionRegistryTests, ReleaseOwnerRetainsSessionUntilLogoutTimeout) { ASSERT_FALSE(weakSession.expired()); const size_t writesBeforeRelease = rig.bus.WriteCount(); - rig.registry.ReleaseOwner(reinterpret_cast(0xCAFE)); + rig.registry.ReleaseOwner(SessionRegistryRig::Owner()); EXPECT_FALSE(weakSession.expired()); ASSERT_EQ(writesBeforeRelease + 1U, rig.bus.WriteCount()); - EXPECT_FALSE(rig.registry.GetSessionState(handle).has_value()); + EXPECT_FALSE(rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle).has_value()); const auto& logoutWrite = rig.bus.WriteAt(writesBeforeRelease); EXPECT_TRUE(rig.bus.CompleteWrite(logoutWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); @@ -545,7 +603,7 @@ TEST(SBP2SessionRegistryTests, ReleaseOwnerRetainsSessionUntilLogoutTimeout) { rig.AdvanceMs(2'000); EXPECT_TRUE(weakSession.expired()); - EXPECT_FALSE(rig.registry.GetSessionState(handle).has_value()); + EXPECT_FALSE(rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle).has_value()); } TEST(SBP2SessionRegistryTests, ReleaseOwnerDuringPendingLoginCancelsWriteAndDropsSession) { @@ -556,14 +614,14 @@ TEST(SBP2SessionRegistryTests, ReleaseOwnerDuringPendingLoginCancelsWriteAndDrop rig.registry.GetSessionWeakForTesting(handle); ASSERT_FALSE(weakSession.expired()); - ASSERT_TRUE(rig.registry.StartLogin(handle)); + ASSERT_TRUE(rig.registry.StartLogin(SessionRegistryRig::Owner(), handle)); ASSERT_EQ(1u, rig.bus.PendingWriteCount()); const auto loginWrite = rig.bus.WriteAt(0); - rig.registry.ReleaseOwner(reinterpret_cast(0xCAFE)); + rig.registry.ReleaseOwner(SessionRegistryRig::Owner()); EXPECT_TRUE(weakSession.expired()); - EXPECT_FALSE(rig.registry.GetSessionState(handle).has_value()); + EXPECT_FALSE(rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle).has_value()); EXPECT_EQ(0u, rig.bus.PendingWriteCount()); EXPECT_FALSE(rig.bus.CompleteWrite(loginWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); } @@ -586,9 +644,9 @@ TEST(SBP2SessionRegistryTests, ReleaseSessionDuringPendingLogoutRetainsUntilStat ASSERT_EQ(writesBeforeLogout + 1U, rig.bus.WriteCount()); ASSERT_EQ(pendingBeforeLogout + 1U, rig.bus.PendingWriteCount()); - EXPECT_TRUE(rig.registry.ReleaseSession(handle)); + EXPECT_TRUE(rig.registry.ReleaseSession(SessionRegistryRig::Owner(), handle)); EXPECT_FALSE(weakSession.expired()); - EXPECT_FALSE(rig.registry.GetSessionState(handle).has_value()); + EXPECT_FALSE(rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle).has_value()); const auto& logoutWrite = rig.bus.WriteAt(writesBeforeLogout); EXPECT_TRUE(rig.bus.CompleteWrite(logoutWrite.handle, ASFW::Async::AsyncStatus::kSuccess)); @@ -602,7 +660,7 @@ TEST(SBP2SessionRegistryTests, ReleaseSessionDuringPendingLogoutRetainsUntilStat std::span{reinterpret_cast(&status), sizeof(status)}); EXPECT_TRUE(weakSession.expired()); - EXPECT_FALSE(rig.registry.GetSessionState(handle).has_value()); + EXPECT_FALSE(rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle).has_value()); } TEST(SBP2SessionRegistryTests, CreateSessionRejectsDuplicateTargetAcrossOwners) { @@ -610,7 +668,7 @@ TEST(SBP2SessionRegistryTests, CreateSessionRejectsDuplicateTargetAcrossOwners) const uint64_t handle = rig.CreateSession(); ASSERT_NE(0u, handle); - auto duplicate = rig.registry.CreateSession(reinterpret_cast(0xBEEF), + auto duplicate = rig.registry.CreateSession(SessionRegistryRig::OtherOwner(), SessionRegistryRig::kGuid, 0); ASSERT_FALSE(duplicate.has_value()); @@ -623,9 +681,9 @@ TEST(SBP2SessionRegistryTests, CreateSessionRejectsDuplicateTargetUntilLogoutCom rig.LoginSuccessfully(handle); const size_t writesBeforeRelease = rig.bus.WriteCount(); - rig.registry.ReleaseOwner(reinterpret_cast(0xCAFE)); + rig.registry.ReleaseOwner(SessionRegistryRig::Owner()); - auto duplicateWhileLoggingOut = rig.registry.CreateSession(reinterpret_cast(0xBEEF), + auto duplicateWhileLoggingOut = rig.registry.CreateSession(SessionRegistryRig::OtherOwner(), SessionRegistryRig::kGuid, 0); ASSERT_FALSE(duplicateWhileLoggingOut.has_value()); @@ -641,7 +699,7 @@ TEST(SBP2SessionRegistryTests, CreateSessionRejectsDuplicateTargetUntilLogoutCom rig.sessionStatusAddress, std::span{reinterpret_cast(&status), sizeof(status)}); - auto replacement = rig.registry.CreateSession(reinterpret_cast(0xBEEF), + auto replacement = rig.registry.CreateSession(SessionRegistryRig::OtherOwner(), SessionRegistryRig::kGuid, 0); ASSERT_TRUE(replacement.has_value()); @@ -686,12 +744,12 @@ TEST(SBP2SessionRegistryTests, SubmitCommandRejectsCDBLargerThanORBPayloadBudget request.transferLength = 0; request.timeoutMs = 100; - EXPECT_FALSE(rig.registry.SubmitCommand(handle, request)); + EXPECT_FALSE(rig.registry.SubmitCommand(SessionRegistryRig::Owner(), handle, request)); } TEST(SBP2SessionRegistryTests, CreateSessionAcceptsRealSBP2SpecAndVersion) { SessionRegistryRig rig; - auto result = rig.registry.CreateSession(reinterpret_cast(0xCAFE), + auto result = rig.registry.CreateSession(SessionRegistryRig::Owner(), SessionRegistryRig::kGuid, 0); ASSERT_TRUE(result.has_value()); From 00b109ed4506b27ff366916b670d392ea4f425d7 Mon Sep 17 00:00:00 2001 From: gly11 Date: Sun, 31 May 2026 17:25:29 +0800 Subject: [PATCH 14/22] fix(sbp2): release sessions before address ranges --- ASFWDriver/UserClient/Handlers/SBP2Handler.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp b/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp index f6ebce6c..d367a70b 100644 --- a/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp +++ b/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp @@ -151,12 +151,12 @@ class SBP2Handler { } void ReleaseOwner(void* owner) { - if (manager_) { - manager_->ReleaseOwner(owner); - } if (registry_) { registry_->ReleaseOwner(owner); } + if (manager_) { + manager_->ReleaseOwner(owner); + } } // Session management (selectors 53-58) From 1c96fe1aea8ea892e0d39811352bfcfce5bcb365 Mon Sep 17 00:00:00 2001 From: gly11 Date: Sun, 31 May 2026 17:28:30 +0800 Subject: [PATCH 15/22] fix(sbp2): reject short session state outputs --- .../UserClient/Handlers/SBP2Handler.hpp | 17 ++++----- tests/SBP2SessionRegistryTests.cpp | 37 +++++++++++++++++++ 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp b/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp index d367a70b..d3552987 100644 --- a/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp +++ b/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp @@ -201,7 +201,8 @@ class SBP2Handler { if (!registry_) { return kIOReturnNotReady; } - if (!args || !args->scalarInput || args->scalarInputCount < 1) { + if (!args || !args->scalarInput || args->scalarInputCount < 1 || + !args->scalarOutput || args->scalarOutputCount < 5) { return kIOReturnBadArgument; } @@ -212,14 +213,12 @@ class SBP2Handler { } // Return as scalars: loginState, loginID, generation, lastError, reconnectPending - if (args->scalarOutput && args->scalarOutputCount >= 5) { - args->scalarOutput[0] = static_cast(state->loginState); - args->scalarOutput[1] = static_cast(state->loginID); - args->scalarOutput[2] = static_cast(state->generation); - args->scalarOutput[3] = static_cast(static_cast(state->lastError)); - args->scalarOutput[4] = state->reconnectPending ? 1 : 0; - args->scalarOutputCount = 5; - } + args->scalarOutput[0] = static_cast(state->loginState); + args->scalarOutput[1] = static_cast(state->loginID); + args->scalarOutput[2] = static_cast(state->generation); + args->scalarOutput[3] = static_cast(static_cast(state->lastError)); + args->scalarOutput[4] = state->reconnectPending ? 1 : 0; + args->scalarOutputCount = 5; return kIOReturnSuccess; } diff --git a/tests/SBP2SessionRegistryTests.cpp b/tests/SBP2SessionRegistryTests.cpp index 2eda1e2c..6642a5a0 100644 --- a/tests/SBP2SessionRegistryTests.cpp +++ b/tests/SBP2SessionRegistryTests.cpp @@ -6,6 +6,15 @@ #include "ASFWDriver/Protocols/SBP2/SCSICommandSet.hpp" #include "ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp" #include "ASFWDriver/Testing/HostDriverKitStubs.hpp" +#ifndef OSDynamicCast +#define ASFW_TEST_DEFINED_OSDYNAMICCAST 1 +#define OSDynamicCast(type, object) static_cast(object) +#endif +#include "ASFWDriver/UserClient/Handlers/SBP2Handler.hpp" +#ifdef ASFW_TEST_DEFINED_OSDYNAMICCAST +#undef OSDynamicCast +#undef ASFW_TEST_DEFINED_OSDYNAMICCAST +#endif #include "tests/mocks/DeferredFireWireBus.hpp" #include @@ -412,6 +421,34 @@ TEST(SBP2SessionRegistryTests, RejectsSessionOperationsFromNonOwningClient) { EXPECT_TRUE(rig.registry.GetSessionState(SessionRegistryRig::Owner(), handle).has_value()); } +TEST(SBP2SessionRegistryTests, HandlerRejectsMissingOrShortSessionStateOutput) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + ASFW::UserClient::SBP2Handler handler(nullptr, &rig.registry); + uint64_t scalarInput[] = {handle}; + + IOUserClientMethodArguments args{}; + args.scalarInput = scalarInput; + args.scalarInputCount = 1; + EXPECT_EQ(kIOReturnBadArgument, + handler.GetSBP2SessionState(&args, SessionRegistryRig::Owner())); + + uint64_t shortOutput[4]{}; + args.scalarOutput = shortOutput; + args.scalarOutputCount = 4; + EXPECT_EQ(kIOReturnBadArgument, + handler.GetSBP2SessionState(&args, SessionRegistryRig::Owner())); + + uint64_t output[5]{}; + args.scalarOutput = output; + args.scalarOutputCount = 5; + EXPECT_EQ(kIOReturnSuccess, + handler.GetSBP2SessionState(&args, SessionRegistryRig::Owner())); + EXPECT_EQ(5u, args.scalarOutputCount); +} + TEST(SBP2SessionRegistryTests, SubmitTaskManagementWritesLogicalUnitResetORB) { SessionRegistryRig rig; const uint64_t handle = rig.CreateSession(); From 11ddc2247c22223387dd02e16e5e6d47f480c81a Mon Sep 17 00:00:00 2001 From: gly11 Date: Sun, 31 May 2026 17:31:43 +0800 Subject: [PATCH 16/22] fix(sbp2): harden command user client ABI --- .../UserClient/Handlers/SBP2Handler.hpp | 23 ++++++- .../WireFormats/SBP2CommandWireFormats.hpp | 22 +++++++ tests/SBP2SessionRegistryTests.cpp | 64 +++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) diff --git a/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp b/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp index d3552987..d628473a 100644 --- a/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp +++ b/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp @@ -290,11 +290,20 @@ class SBP2Handler { } const auto* header = reinterpret_cast(bytes); + if (header->cdbLength == 0 || + header->cdbLength > Wire::kSBP2CommandMaxCDBLength || + header->transferLength > Wire::kSBP2CommandMaxTransferLength || + header->captureSenseData > 1 || + header->_reserved[0] != 0 || + header->_reserved[1] != 0) { + return kIOReturnBadArgument; + } + const size_t expectedLength = sizeof(Wire::SBP2CommandRequestWire) + static_cast(header->cdbLength) + static_cast(header->outgoingLength); - if (inputLength != expectedLength || header->cdbLength == 0) { + if (inputLength != expectedLength) { return kIOReturnBadArgument; } @@ -313,6 +322,18 @@ class SBP2Handler { return kIOReturnBadArgument; } + if (direction == Protocols::SBP2::SCSI::DataDirection::ToTarget) { + if (header->outgoingLength != header->transferLength) { + return kIOReturnBadArgument; + } + } else if (header->outgoingLength != 0) { + return kIOReturnBadArgument; + } + if (direction == Protocols::SBP2::SCSI::DataDirection::None && + header->transferLength != 0) { + return kIOReturnBadArgument; + } + Protocols::SBP2::SCSI::CommandRequest request{}; request.direction = direction; request.transferLength = header->transferLength; diff --git a/ASFWDriver/UserClient/WireFormats/SBP2CommandWireFormats.hpp b/ASFWDriver/UserClient/WireFormats/SBP2CommandWireFormats.hpp index 3564e4d3..a29e286c 100644 --- a/ASFWDriver/UserClient/WireFormats/SBP2CommandWireFormats.hpp +++ b/ASFWDriver/UserClient/WireFormats/SBP2CommandWireFormats.hpp @@ -1,9 +1,15 @@ #pragma once +#include #include namespace ASFW::UserClient::Wire { +inline constexpr uint32_t kSBP2CommandMaxCDBLength = 16; +inline constexpr uint32_t kSBP2CommandMaxTransferLength = 16u * 1024u * 1024u; + +// DriverKit user-client ABI records. These are native-endian host records, +// not SBP-2 big-endian bus wire structures. struct __attribute__((packed)) SBP2CommandRequestWire { uint32_t cdbLength; uint32_t transferLength; @@ -24,4 +30,20 @@ struct __attribute__((packed)) SBP2CommandResultWire { // Followed by: payload bytes, then sense bytes. }; +static_assert(sizeof(SBP2CommandRequestWire) == 20); +static_assert(offsetof(SBP2CommandRequestWire, cdbLength) == 0); +static_assert(offsetof(SBP2CommandRequestWire, transferLength) == 4); +static_assert(offsetof(SBP2CommandRequestWire, outgoingLength) == 8); +static_assert(offsetof(SBP2CommandRequestWire, timeoutMs) == 12); +static_assert(offsetof(SBP2CommandRequestWire, direction) == 16); +static_assert(offsetof(SBP2CommandRequestWire, captureSenseData) == 17); +static_assert(offsetof(SBP2CommandRequestWire, _reserved) == 18); + +static_assert(sizeof(SBP2CommandResultWire) == 16); +static_assert(offsetof(SBP2CommandResultWire, transportStatus) == 0); +static_assert(offsetof(SBP2CommandResultWire, sbpStatus) == 4); +static_assert(offsetof(SBP2CommandResultWire, _reserved) == 5); +static_assert(offsetof(SBP2CommandResultWire, payloadLength) == 8); +static_assert(offsetof(SBP2CommandResultWire, senseLength) == 12); + } // namespace ASFW::UserClient::Wire diff --git a/tests/SBP2SessionRegistryTests.cpp b/tests/SBP2SessionRegistryTests.cpp index 6642a5a0..a2bd199f 100644 --- a/tests/SBP2SessionRegistryTests.cpp +++ b/tests/SBP2SessionRegistryTests.cpp @@ -19,6 +19,7 @@ #include #include +#include #include namespace { @@ -50,6 +51,7 @@ using ASFW::Protocols::SBP2::Wire::TaskManagementORB; using ASFW::Protocols::SBP2::Wire::ToBE16; using ASFW::Protocols::SBP2::Wire::ToBE32; namespace SBPStatus = ASFW::Protocols::SBP2::Wire::SBPStatus; +namespace UCWire = ASFW::UserClient::Wire; uint64_t ComposeAddress(uint16_t hi, uint32_t lo) { return (static_cast(hi) << 32) | lo; @@ -122,6 +124,35 @@ void CompleteTaskManagementStatus(AddressSpaceManager& manager, std::span{reinterpret_cast(&status), sizeof(status)}); } +std::vector BuildCommandRequestWire(std::vector cdb, + uint32_t transferLength = 0, + uint8_t direction = 0, + std::vector outgoingPayload = {}, + uint8_t captureSenseData = 0, + uint8_t reserved0 = 0, + uint8_t reserved1 = 0) { + UCWire::SBP2CommandRequestWire header{}; + header.cdbLength = static_cast(cdb.size()); + header.transferLength = transferLength; + header.outgoingLength = static_cast(outgoingPayload.size()); + header.direction = direction; + header.captureSenseData = captureSenseData; + header._reserved[0] = reserved0; + header._reserved[1] = reserved1; + + std::vector serialized(sizeof(header) + cdb.size() + outgoingPayload.size()); + std::memcpy(serialized.data(), &header, sizeof(header)); + size_t offset = sizeof(header); + if (!cdb.empty()) { + std::memcpy(serialized.data() + offset, cdb.data(), cdb.size()); + offset += cdb.size(); + } + if (!outgoingPayload.empty()) { + std::memcpy(serialized.data() + offset, outgoingPayload.data(), outgoingPayload.size()); + } + return serialized; +} + class SessionRegistryRig { public: explicit SessionRegistryRig(uint32_t unitCharacteristics = 0x080400) @@ -449,6 +480,39 @@ TEST(SBP2SessionRegistryTests, HandlerRejectsMissingOrShortSessionStateOutput) { EXPECT_EQ(5u, args.scalarOutputCount); } +TEST(SBP2SessionRegistryTests, HandlerHardensCommandABIInputsBeforeSubmission) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + ASFW::UserClient::SBP2Handler handler(nullptr, &rig.registry); + uint64_t scalarInput[] = {handle}; + + auto submit = [&](const std::vector& serialized) { + std::unique_ptr input(OSData::withBytes(serialized.data(), + static_cast(serialized.size()))); + IOUserClientMethodArguments args{}; + args.scalarInput = scalarInput; + args.scalarInputCount = 1; + args.structureInput = input.get(); + return handler.SubmitSBP2Command(&args, SessionRegistryRig::Owner()); + }; + + EXPECT_EQ(kIOReturnBadArgument, + submit(BuildCommandRequestWire(std::vector(17, 0x00)))); + EXPECT_EQ(kIOReturnBadArgument, + submit(BuildCommandRequestWire({0x00}, 0, 0, {}, 2))); + EXPECT_EQ(kIOReturnBadArgument, + submit(BuildCommandRequestWire({0x00}, 0, 0, {}, 0, 1))); + EXPECT_EQ(kIOReturnBadArgument, + submit(BuildCommandRequestWire({0x2A}, UCWire::kSBP2CommandMaxTransferLength + 1, 1))); + EXPECT_EQ(kIOReturnBadArgument, + submit(BuildCommandRequestWire({0x2A}, 4, 2, {0x01, 0x02}))); + EXPECT_EQ(kIOReturnBadArgument, + submit(BuildCommandRequestWire({0x2A}, 4, 1, {0x01}))); + EXPECT_EQ(kIOReturnSuccess, submit(BuildCommandRequestWire({0x00}))); +} + TEST(SBP2SessionRegistryTests, SubmitTaskManagementWritesLogicalUnitResetORB) { SessionRegistryRig rig; const uint64_t handle = rig.CreateSession(); From 71e7b25167d08eda66ac702c84cd61825c354b5b Mon Sep 17 00:00:00 2001 From: gly11 Date: Sun, 31 May 2026 17:33:13 +0800 Subject: [PATCH 17/22] docs(sbp2): clarify session registry handles --- .../Protocols/SBP2/SBP2SessionRegistry.hpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp index b1ec3ca4..56ada706 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp @@ -1,8 +1,9 @@ #pragma once // SBP-2 Session Registry — bridges discovery metadata to SBP2LoginSession instances. -// Owns sessions keyed by (guid, romOffset), handles bus-reset suspend/reconnect, -// and provides the INQUIRY command job for the v1 vertical slice. +// Sessions are created for an owner and target (guid, romOffset). Public +// operations use opaque handles plus owner validation, while the registry also +// rejects duplicate live targets by (guid, romOffset). #include "SBP2LoginSession.hpp" #include "SBP2CommandORB.hpp" @@ -70,16 +71,17 @@ class SBP2SessionRegistry { SBP2SessionRegistry(const SBP2SessionRegistry&) = delete; SBP2SessionRegistry& operator=(const SBP2SessionRegistry&) = delete; - // Create a session for (owner, guid, romOffset). - // Validates the unit is SBP-2 and has Management_Agent_Offset. + // Create an owner-bound session for target (guid, romOffset). + // Validates the unit is SBP-2, has Management_Agent_Offset, and is not + // already represented by another live or retiring session. [[nodiscard]] std::expected CreateSession(void* owner, uint64_t guid, uint32_t romOffset); - // Start login for a session. Returns false if not in Idle state. + // Start login for an owner-bound handle. Returns false if not in Idle state. [[nodiscard]] bool StartLogin(void* owner, uint64_t handle); - // Get session state. Returns nullopt if handle not found. + // Get session state. Returns nullopt if owner/handle is not found. [[nodiscard]] std::optional GetSessionState(void* owner, uint64_t handle) const; @@ -106,7 +108,7 @@ class SBP2SessionRegistry { uint64_t handle, SBP2ManagementORB::Function function); - // Release a specific session. + // Release a specific owner-bound session. [[nodiscard]] bool ReleaseSession(void* owner, uint64_t handle); // Release all sessions for an owner (best-effort logout + cleanup). From 4d50ee436f4cdda835b122ad6e044b351582a52a Mon Sep 17 00:00:00 2001 From: gly11 Date: Sun, 31 May 2026 19:02:26 +0800 Subject: [PATCH 18/22] fix(sbp2): harden address-space bounds and labels --- .../Protocols/SBP2/AddressSpaceManager.hpp | 31 +++++++++++++++---- tests/AddressSpaceManagerTests.cpp | 25 +++++++++++++++ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp b/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp index 6410683b..44e1b2dc 100644 --- a/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp +++ b/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -229,6 +230,9 @@ class AddressSpaceManager { if (!lock_) { return kIOReturnBadArgument; } + if (data.size() > std::numeric_limits::max()) { + return kIOReturnBadArgument; + } IOLockLock(lock_); auto it = ranges_.find(handle); @@ -256,6 +260,9 @@ class AddressSpaceManager { if (!lock_ || payload.empty()) { return Async::ResponseCode::AddressError; } + if (payload.size() > std::numeric_limits::max()) { + return Async::ResponseCode::DataError; + } RemoteWriteCallback callback; uint64_t handle = 0; @@ -275,7 +282,7 @@ class AddressSpaceManager { "AddressSpaceManager[%p] remote write label=%s addr=0x%012llx len=%zu src=%p " "handle=0x%llx rangeAddr=0x%012llx off=%u buf=%p mapped=%p backing=%u", this, - range->debugLabel, + DebugLabelCString(*range), static_cast(address), payload.size(), payload.data(), @@ -333,7 +340,7 @@ class AddressSpaceManager { "AddressSpaceManager[%p] remote read-block label=%s addr=0x%012llx len=%u " "handle=0x%llx rangeAddr=0x%012llx off=%llu dma=0x%08x", this, - range->debugLabel, + DebugLabelCString(*range), static_cast(address), length, static_cast(range->meta.handle), @@ -366,7 +373,7 @@ class AddressSpaceManager { "AddressSpaceManager[%p] remote read-quadlet label=%s addr=0x%012llx " "handle=0x%llx rangeAddr=0x%012llx off=%u value=0x%08x", this, - range->debugLabel, + DebugLabelCString(*range), static_cast(address), static_cast(range->meta.handle), static_cast(range->meta.address), @@ -424,7 +431,7 @@ class AddressSpaceManager { IOLockLock(lock_); auto it = ranges_.find(handle); if (it != ranges_.end()) { - it->second.debugLabel = label != nullptr ? label : "unlabeled"; + CopyDebugLabel(it->second, label); } IOLockUnlock(lock_); } @@ -447,13 +454,14 @@ class AddressSpaceManager { static constexpr uint32_t kAutoAddressWindowStartLo = 0x0010'0000u; static constexpr uint32_t kAutoAddressWindowEndLo = 0x0FFF'FFFFu; static constexpr uint64_t kAutoAddressAlignment = 8ULL; + static constexpr std::size_t kDebugLabelCapacity = 64; struct AddressRange { AddressRangeMeta meta{}; void* owner{nullptr}; std::vector buffer; RemoteWriteCallback onRemoteWrite; - const char* debugLabel{"unlabeled"}; + std::array debugLabel{}; OSSharedPtr descriptor{}; OSSharedPtr dmaCommand{}; @@ -491,6 +499,16 @@ class AddressSpaceManager { return end <= static_cast(range.meta.length); } + static void CopyDebugLabel(AddressRange& range, const char* label) { + const char* source = label != nullptr ? label : "unlabeled"; + range.debugLabel.fill('\0'); + std::strncpy(range.debugLabel.data(), source, range.debugLabel.size() - 1); + } + + static const char* DebugLabelCString(const AddressRange& range) { + return range.debugLabel[0] != '\0' ? range.debugLabel.data() : "unlabeled"; + } + AddressRange* FindRangeByAddressLocked(uint64_t address, uint32_t length) { const uint64_t end = address + static_cast(length); if (end < address) { @@ -525,7 +543,7 @@ class AddressSpaceManager { ASFW_ADDRSPACE_LOG( "AddressSpaceManager[%p] range label=%s handle=0x%llx owner=%p addr=0x%012llx len=%u backing=%u dma=0x%08x", this, - range.debugLabel, + DebugLabelCString(range), static_cast(range.meta.handle), range.owner, static_cast(range.meta.address), @@ -557,6 +575,7 @@ class AddressSpaceManager { } AddressRange range{}; + CopyDebugLabel(range, nullptr); range.owner = owner; range.meta.handle = nextHandle_++; range.meta.address = start; diff --git a/tests/AddressSpaceManagerTests.cpp b/tests/AddressSpaceManagerTests.cpp index 2ba13fe0..12109c87 100644 --- a/tests/AddressSpaceManagerTests.cpp +++ b/tests/AddressSpaceManagerTests.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp" @@ -296,6 +297,30 @@ TEST(AddressSpaceManagerTests, ClearingRemoteWriteCallbackStopsFurtherNotificati EXPECT_EQ(1, callbackCount); } +TEST(AddressSpaceManagerTests, WriteLocalDataRejectsSizeTooLargeForWireLength) { + ASFW::Protocols::SBP2::AddressSpaceManager manager(nullptr); + + constexpr std::size_t tooLarge = + static_cast(std::numeric_limits::max()) + 1u; + const auto hugePayload = + std::span(reinterpret_cast(0x1000), tooLarge); + + EXPECT_EQ(kIOReturnBadArgument, + manager.WriteLocalData(reinterpret_cast(0xE), 1, 0, hugePayload)); +} + +TEST(AddressSpaceManagerTests, ApplyRemoteWriteRejectsSizeTooLargeForWireLength) { + ASFW::Protocols::SBP2::AddressSpaceManager manager(nullptr); + + constexpr std::size_t tooLarge = + static_cast(std::numeric_limits::max()) + 1u; + const auto hugePayload = + std::span(reinterpret_cast(0x1000), tooLarge); + + EXPECT_EQ(ASFW::Async::ResponseCode::DataError, + manager.ApplyRemoteWrite(ComposeAddress(0xFFFF, 0x0050'0000), hugePayload)); +} + TEST(AddressSpaceManagerTests, AutoAllocationRejectsRequestLargerThanWindow) { ASFW::Protocols::SBP2::AddressSpaceManager manager(nullptr); From c53fcd2fd1d2af5ae108f5bc2157e14d94c2d483 Mon Sep 17 00:00:00 2001 From: gly11 Date: Sun, 31 May 2026 19:04:51 +0800 Subject: [PATCH 19/22] fix(sbp2): guard remote-write callback lifetimes --- .../Protocols/SBP2/AddressSpaceManager.hpp | 41 ++++++++++++++++--- .../Protocols/SBP2/SBP2LoginSession.cpp | 2 + .../Protocols/SBP2/SBP2ManagementORB.cpp | 2 + 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp b/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp index 44e1b2dc..21908533 100644 --- a/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp +++ b/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -33,9 +34,20 @@ class AddressSpaceManager { public: // Callback invoked when a remote write arrives for a registered range. // Parameters: handle, offset within range, payload data. + // + // AddressSpaceManager invokes callbacks outside its lock. Callback targets + // must capture weak/shared lifetime state, not raw owning object pointers. using RemoteWriteCallback = std::function payload)>; + struct RemoteWriteCallbackSlot { + explicit RemoteWriteCallbackSlot(RemoteWriteCallback callbackIn) + : callback(std::move(callbackIn)) {} + + RemoteWriteCallback callback; + std::atomic enabled{true}; + }; + struct AddressRangeMeta { uint64_t handle{0}; uint64_t address{0}; @@ -264,7 +276,7 @@ class AddressSpaceManager { return Async::ResponseCode::DataError; } - RemoteWriteCallback callback; + std::shared_ptr callbackSlot; uint64_t handle = 0; uint32_t offset = 0; @@ -293,14 +305,15 @@ class AddressSpaceManager { range->mappedBytes, range->hasBacking ? 1u : 0u); WriteBytesLocked(*range, offset, payload); - callback = range->onRemoteWrite; + callbackSlot = range->remoteWriteSlot; handle = range->meta.handle; IOLockUnlock(lock_); } // Fire callback outside lock to avoid deadlock. - if (callback) { - callback(handle, offset, payload); + if (callbackSlot && callbackSlot->enabled.load(std::memory_order_acquire) && + callbackSlot->callback) { + callbackSlot->callback(handle, offset, payload); } return Async::ResponseCode::Complete; @@ -410,6 +423,10 @@ class AddressSpaceManager { // Register a callback to fire when a remote write arrives for the given handle. // Must be called after AllocateAddressRange. Replaces any previous callback. + // + // Replacing or clearing the callback invalidates callbacks copied but not + // yet dispatched. Callback targets must still use weak/shared lifetime + // capture because callbacks run outside the manager lock. void SetRemoteWriteCallback(uint64_t handle, RemoteWriteCallback callback) { if (!lock_ || handle == 0) { return; @@ -418,7 +435,7 @@ class AddressSpaceManager { IOLockLock(lock_); auto it = ranges_.find(handle); if (it != ranges_.end()) { - it->second.onRemoteWrite = std::move(callback); + ReplaceRemoteWriteCallbackLocked(it->second, std::move(callback)); } IOLockUnlock(lock_); } @@ -460,7 +477,7 @@ class AddressSpaceManager { AddressRangeMeta meta{}; void* owner{nullptr}; std::vector buffer; - RemoteWriteCallback onRemoteWrite; + std::shared_ptr remoteWriteSlot; std::array debugLabel{}; OSSharedPtr descriptor{}; @@ -509,6 +526,18 @@ class AddressSpaceManager { return range.debugLabel[0] != '\0' ? range.debugLabel.data() : "unlabeled"; } + static void ReplaceRemoteWriteCallbackLocked(AddressRange& range, + RemoteWriteCallback callback) { + if (range.remoteWriteSlot) { + range.remoteWriteSlot->enabled.store(false, std::memory_order_release); + range.remoteWriteSlot.reset(); + } + if (callback) { + range.remoteWriteSlot = + std::make_shared(std::move(callback)); + } + } + AddressRange* FindRangeByAddressLocked(uint64_t address, uint32_t length) { const uint64_t end = address + static_cast(length); if (end < address) { diff --git a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp index ea759e92..ba48a601 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp @@ -381,6 +381,7 @@ bool SBP2LoginSession::AllocateResources() noexcept { // Re-register callback in case it was previously cleared by a reset path. if (statusBlockHandle_ != 0) { const std::weak_ptr weakSelf = weak_from_this(); + // AddressSpaceManager dispatches outside its lock, so never capture raw this. addrSpaceMgr_.SetRemoteWriteCallback( statusBlockHandle_, [weakSelf](uint64_t /*handle*/, uint32_t offset, std::span payload) { @@ -432,6 +433,7 @@ bool SBP2LoginSession::AllocateResources() noexcept { // here to signal login/reconnect/logout completion (and ORB completion // in Step 2). const std::weak_ptr weakSelf = weak_from_this(); + // AddressSpaceManager dispatches outside its lock, so never capture raw this. addrSpaceMgr_.SetRemoteWriteCallback( statusBlockHandle_, [weakSelf](uint64_t /*handle*/, uint32_t offset, std::span payload) { diff --git a/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.cpp b/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.cpp index 767454eb..d3d4b043 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.cpp @@ -195,6 +195,8 @@ SBP2ManagementORB::~SBP2ManagementORB() { bool SBP2ManagementORB::AllocateResources() noexcept { const auto state = asyncState_; const auto registerStatusWriteCallback = [this, state]() { + // AddressSpaceManager dispatches outside its lock; the shared state token + // keeps callback lifetime independent from this ORB object. addrMgr_.SetRemoteWriteCallback( statusBlockHandle_, [state](uint64_t /*handle*/, uint32_t offset, std::span payload) { From 98b0a904328fbd1512637e00b5c3a3e2ea669b06 Mon Sep 17 00:00:00 2001 From: gly11 Date: Sun, 31 May 2026 19:09:02 +0800 Subject: [PATCH 20/22] fix(sbp2): retain read-block response backing --- ASFWDriver/Async/AsyncSubsystemInterrupts.cpp | 4 + ASFWDriver/Async/AsyncSubsystemLifecycle.cpp | 5 +- ASFWDriver/Async/Tx/ResponseSender.cpp | 29 +++++- ASFWDriver/Async/Tx/ResponseSender.hpp | 17 +++- .../Protocols/SBP2/AddressSpaceManager.hpp | 98 +++++++++++-------- ASFWDriver/Service/DriverContext.cpp | 5 +- tests/ResponseSenderStub.cpp | 14 ++- 7 files changed, 123 insertions(+), 49 deletions(-) diff --git a/ASFWDriver/Async/AsyncSubsystemInterrupts.cpp b/ASFWDriver/Async/AsyncSubsystemInterrupts.cpp index afab073c..b3c78a52 100644 --- a/ASFWDriver/Async/AsyncSubsystemInterrupts.cpp +++ b/ASFWDriver/Async/AsyncSubsystemInterrupts.cpp @@ -2,6 +2,7 @@ #include "Contexts/ATRequestContext.hpp" #include "Contexts/ATResponseContext.hpp" +#include "Tx/ResponseSender.hpp" #include "../Logging/Logging.hpp" #include "../Shared/Memory/DMAMemoryManager.hpp" @@ -22,6 +23,9 @@ uint32_t AsyncSubsystem::DrainTxCompletions(const char* reason) { return; } while (auto completion = ctx->ScanCompletion()) { + if (completion->isResponseContext && responseSender_) { + responseSender_->OnTxCompletion(*completion); + } tracking_->OnTxCompletion(*completion); ++drained; } diff --git a/ASFWDriver/Async/AsyncSubsystemLifecycle.cpp b/ASFWDriver/Async/AsyncSubsystemLifecycle.cpp index 757c9433..234a5ae3 100644 --- a/ASFWDriver/Async/AsyncSubsystemLifecycle.cpp +++ b/ASFWDriver/Async/AsyncSubsystemLifecycle.cpp @@ -563,7 +563,10 @@ void AsyncSubsystem::Teardown(bool disableHardware) { txnMgr_.reset(); } - responseSender_.reset(); + if (responseSender_) { + responseSender_->ClearOutstandingResponses(); + responseSender_.reset(); + } descriptorBuilder_ = nullptr; descriptorBuilderResponse_ = nullptr; packetBuilder_.reset(); diff --git a/ASFWDriver/Async/Tx/ResponseSender.cpp b/ASFWDriver/Async/Tx/ResponseSender.cpp index 7d114b55..97f7f020 100644 --- a/ASFWDriver/Async/Tx/ResponseSender.cpp +++ b/ASFWDriver/Async/Tx/ResponseSender.cpp @@ -2,6 +2,7 @@ #include "../Engine/ContextManager.hpp" #include "../Contexts/ATResponseContext.hpp" +#include "../Track/TxCompletion.hpp" #include "../Tx/DescriptorBuilder.hpp" #include "../Tx/Submitter.hpp" #include "../../Bus/GenerationTracker.hpp" @@ -50,7 +51,8 @@ void ResponseSender::SendResponse(const ARPacketView& request, const uint32_t* header, std::size_t headerBytes, uint64_t payloadDeviceAddress, - std::size_t payloadLength) noexcept { + std::size_t payloadLength, + std::shared_ptr payloadLease) noexcept { // Per IEEE 1394, broadcast requests (destID=0xFFFF) do not get responses. if (request.destID == 0xFFFF) { ASFW_LOG_V3(Async, "ResponseSender: skip response for broadcast destID=0xFFFF"); @@ -78,8 +80,16 @@ void ResponseSender::SendResponse(const ARPacketView& request, return; } + const auto* leaseKey = chain.last; + if (payloadLease && leaseKey) { + responsePayloadLeases_[leaseKey] = std::move(payloadLease); + } + const auto submitRes = submitter_.submit_tx_chain(atRspCtx, std::move(chain)); if (submitRes.kr != kIOReturnSuccess) { + if (leaseKey) { + responsePayloadLeases_.erase(leaseKey); + } ASFW_LOG_ERROR( Async, "ResponseSender: submit_tx_chain failed (tCode=0x%x kr=0x%x)", @@ -147,7 +157,8 @@ void ResponseSender::SendReadQuadletResponse(const ARPacketView& request, void ResponseSender::SendReadBlockResponse(const ARPacketView& request, ResponseCode rcode, uint64_t payloadDeviceAddress, - uint32_t payloadLength) noexcept { + uint32_t payloadLength, + std::shared_ptr payloadLease) noexcept { if (request.tCode != 0x5) { ASFW_LOG_V3(Async, "ResponseSender: skip RdBlockResp for non-read-block tCode=0x%x", @@ -177,7 +188,19 @@ void ResponseSender::SendReadBlockResponse(const ARPacketView& request, header, sizeof(header), responsePayloadAddress, - responsePayloadLen); + responsePayloadLen, + std::move(payloadLease)); +} + +void ResponseSender::OnTxCompletion(const TxCompletion& completion) noexcept { + if (!completion.isResponseContext || !completion.descriptor) { + return; + } + responsePayloadLeases_.erase(completion.descriptor); +} + +void ResponseSender::ClearOutstandingResponses() noexcept { + responsePayloadLeases_.clear(); } } // namespace ASFW::Async diff --git a/ASFWDriver/Async/Tx/ResponseSender.hpp b/ASFWDriver/Async/Tx/ResponseSender.hpp index 5a1b80ea..a4e475d2 100644 --- a/ASFWDriver/Async/Tx/ResponseSender.hpp +++ b/ASFWDriver/Async/Tx/ResponseSender.hpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include "../ResponseCode.hpp" #include "../Rx/PacketRouter.hpp" @@ -11,9 +13,11 @@ namespace ASFW::Async { class DescriptorBuilder; class ATResponseContext; class IFireWireBusInfo; +struct TxCompletion; namespace Bus { class GenerationTracker; } namespace Engine { class ContextManager; } namespace Tx { class Submitter; } +namespace HW { struct OHCIDescriptor; } /// Utility to build and send Write Response (WrResp) packets for incoming AR requests. class ResponseSender { @@ -36,7 +40,14 @@ class ResponseSender { void SendReadBlockResponse(const ARPacketView& request, ResponseCode rcode, uint64_t payloadDeviceAddress, - uint32_t payloadLength) noexcept; + uint32_t payloadLength, + std::shared_ptr payloadLease = {}) noexcept; + + /// Release response payload leases after AT response descriptor completion. + void OnTxCompletion(const TxCompletion& completion) noexcept; + + /// Drop any retained response payload leases during response subsystem teardown. + void ClearOutstandingResponses() noexcept; private: void SendResponse(const ARPacketView& request, @@ -45,12 +56,14 @@ class ResponseSender { const uint32_t* header, std::size_t headerBytes, uint64_t payloadDeviceAddress, - std::size_t payloadLength) noexcept; + std::size_t payloadLength, + std::shared_ptr payloadLease = {}) noexcept; DescriptorBuilder& builder_; Tx::Submitter& submitter_; Engine::ContextManager& ctxMgr_; Bus::GenerationTracker& generationTracker_; + std::unordered_map> responsePayloadLeases_; }; } // namespace ASFW::Async diff --git a/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp b/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp index 21908533..819dfc8e 100644 --- a/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp +++ b/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp @@ -59,6 +59,7 @@ class AddressSpaceManager { struct ReadSlice { uint64_t payloadDeviceAddress{0}; uint32_t payloadLength{0}; + std::shared_ptr backingLease{}; }; explicit AddressSpaceManager(ASFW::Driver::HardwareInterface* hardware) noexcept @@ -302,8 +303,8 @@ class AddressSpaceManager { static_cast(range->meta.address), offset, range->buffer.data(), - range->mappedBytes, - range->hasBacking ? 1u : 0u); + range->backing ? range->backing->mappedBytes : nullptr, + range->backing && range->backing->hasBacking ? 1u : 0u); WriteBytesLocked(*range, offset, payload); callbackSlot = range->remoteWriteSlot; handle = range->meta.handle; @@ -334,13 +335,14 @@ class AddressSpaceManager { return Async::ResponseCode::AddressError; } - if (!range->hasBacking || range->deviceAddress == 0) { + if (!range->backing || !range->backing->hasBacking || + range->backing->deviceAddress == 0) { IOLockUnlock(lock_); return Async::ResponseCode::DataError; } const uint64_t offset = address - range->meta.address; - const uint64_t payloadAddress = range->deviceAddress + offset; + const uint64_t payloadAddress = range->backing->deviceAddress + offset; if (payloadAddress > 0xFFFF'FFFFULL) { IOLockUnlock(lock_); return Async::ResponseCode::DataError; @@ -348,6 +350,7 @@ class AddressSpaceManager { outSlice->payloadDeviceAddress = payloadAddress; outSlice->payloadLength = length; + outSlice->backingLease = range->backing; ASFW_ADDRSPACE_LOG( "AddressSpaceManager[%p] remote read-block label=%s addr=0x%012llx len=%u " @@ -473,12 +476,27 @@ class AddressSpaceManager { static constexpr uint64_t kAutoAddressAlignment = 8ULL; static constexpr std::size_t kDebugLabelCapacity = 64; - struct AddressRange { - AddressRangeMeta meta{}; - void* owner{nullptr}; - std::vector buffer; - std::shared_ptr remoteWriteSlot; - std::array debugLabel{}; + struct AddressRangeBacking { + ~AddressRangeBacking() { + if (mapping) { + mapping->release(); + mapping = nullptr; + } + + if (dmaCommand) { + dmaCommand->CompleteDMA(kIODMACommandCompleteDMANoOptions); + dmaCommand.reset(); + } + + descriptor.reset(); + mappedBytes = nullptr; + deviceAddress = 0; + hasBacking = false; + } + + AddressRangeBacking() = default; + AddressRangeBacking(const AddressRangeBacking&) = delete; + AddressRangeBacking& operator=(const AddressRangeBacking&) = delete; OSSharedPtr descriptor{}; OSSharedPtr dmaCommand{}; @@ -488,6 +506,15 @@ class AddressSpaceManager { bool hasBacking{false}; }; + struct AddressRange { + AddressRangeMeta meta{}; + void* owner{nullptr}; + std::vector buffer; + std::shared_ptr remoteWriteSlot; + std::array debugLabel{}; + std::shared_ptr backing{}; + }; + static uint64_t ComposeAddress(uint16_t hi, uint32_t lo) { return (static_cast(hi) << 32) | static_cast(lo); } @@ -577,8 +604,8 @@ class AddressSpaceManager { range.owner, static_cast(range.meta.address), range.meta.length, - range.hasBacking ? 1u : 0u, - static_cast(range.deviceAddress)); + range.backing && range.backing->hasBacking ? 1u : 0u, + static_cast(range.backing ? range.backing->deviceAddress : 0)); } } @@ -649,7 +676,7 @@ class AddressSpaceManager { if (!dma.has_value()) { #ifdef ASFW_HOST_TEST - range.hasBacking = false; + range.backing.reset(); return kIOReturnSuccess; #else ASFW_LOG(UserClient, @@ -688,41 +715,30 @@ class AddressSpaceManager { std::memset(mapped, 0, backingSize); OSSynchronizeIO(); - range.descriptor = std::move(dma->descriptor); - range.dmaCommand = std::move(dma->dmaCommand); - range.mapping = mapping; - range.mappedBytes = mapped; - range.deviceAddress = dma->deviceAddress; - range.hasBacking = true; + auto backing = std::make_shared(); + backing->descriptor = std::move(dma->descriptor); + backing->dmaCommand = std::move(dma->dmaCommand); + backing->mapping = mapping; + backing->mappedBytes = mapped; + backing->deviceAddress = dma->deviceAddress; + backing->hasBacking = true; + range.backing = std::move(backing); return kIOReturnSuccess; } static void CleanupBacking(AddressRange& range) { - if (range.mapping) { - range.mapping->release(); - range.mapping = nullptr; - } - - if (range.dmaCommand) { - range.dmaCommand->CompleteDMA(kIODMACommandCompleteDMANoOptions); - range.dmaCommand.reset(); - } - - range.descriptor.reset(); - range.mappedBytes = nullptr; - range.deviceAddress = 0; - range.hasBacking = false; + range.backing.reset(); } static void SyncRange(AddressRange& range, uint64_t offset, uint64_t length) { - if (!range.hasBacking) { + if (!range.backing || !range.backing->hasBacking) { return; } #if defined(IODMACommand_Synchronize_ID) - if (range.dmaCommand) { - const kern_return_t syncKr = range.dmaCommand->Synchronize( + if (range.backing->dmaCommand) { + const kern_return_t syncKr = range.backing->dmaCommand->Synchronize( 0, offset, length); @@ -770,14 +786,16 @@ class AddressSpaceManager { data.size(), data.data(), range.buffer.data(), - range.mappedBytes, + range.backing ? range.backing->mappedBytes : nullptr, static_cast(reinterpret_cast(data.data()) & 0x7ULL), static_cast(reinterpret_cast(range.buffer.data()) & 0x7ULL), - static_cast(reinterpret_cast(range.mappedBytes) & 0x7ULL)); + static_cast( + reinterpret_cast(range.backing ? range.backing->mappedBytes : nullptr) & + 0x7ULL)); CopyPayloadBytes(range.buffer.data() + static_cast(offset), data); - if (range.hasBacking && range.mappedBytes) { - CopyPayloadBytes(range.mappedBytes + static_cast(offset), data); + if (range.backing && range.backing->hasBacking && range.backing->mappedBytes) { + CopyPayloadBytes(range.backing->mappedBytes + static_cast(offset), data); std::atomic_thread_fence(std::memory_order_release); SyncRange(range, offset, data.size()); } diff --git a/ASFWDriver/Service/DriverContext.cpp b/ASFWDriver/Service/DriverContext.cpp index 5532f0f8..cf30695b 100644 --- a/ASFWDriver/Service/DriverContext.cpp +++ b/ASFWDriver/Service/DriverContext.cpp @@ -4,6 +4,8 @@ #include +#include + #include "../Async/AsyncSubsystem.hpp" #include "../Async/Interfaces/IFireWireBus.hpp" #include "../Async/PacketHelpers.hpp" @@ -305,7 +307,8 @@ void DriverWiring::EnsureSbp2Deps(::ServiceContext& ctx) { packet, result, slice.payloadDeviceAddress, - slice.payloadLength); + slice.payloadLength, + std::move(slice.backingLease)); } else { responder->SendReadBlockResponse(packet, result, 0, 0); } diff --git a/tests/ResponseSenderStub.cpp b/tests/ResponseSenderStub.cpp index 452ca885..b848b2e0 100644 --- a/tests/ResponseSenderStub.cpp +++ b/tests/ResponseSenderStub.cpp @@ -28,20 +28,29 @@ void ResponseSender::SendReadQuadletResponse(const ARPacketView& request, void ResponseSender::SendReadBlockResponse(const ARPacketView& request, ResponseCode rcode, uint64_t payloadDeviceAddress, - uint32_t payloadLength) noexcept { + uint32_t payloadLength, + std::shared_ptr payloadLease) noexcept { (void)request; (void)rcode; (void)payloadDeviceAddress; (void)payloadLength; + (void)payloadLease; } +void ResponseSender::OnTxCompletion(const TxCompletion& completion) noexcept { + (void)completion; +} + +void ResponseSender::ClearOutstandingResponses() noexcept {} + void ResponseSender::SendResponse(const ARPacketView& request, ResponseCode rcode, uint8_t responseTCode, const uint32_t* header, std::size_t headerBytes, uint64_t payloadDeviceAddress, - std::size_t payloadLength) noexcept { + std::size_t payloadLength, + std::shared_ptr payloadLease) noexcept { (void)request; (void)rcode; (void)responseTCode; @@ -49,6 +58,7 @@ void ResponseSender::SendResponse(const ARPacketView& request, (void)headerBytes; (void)payloadDeviceAddress; (void)payloadLength; + (void)payloadLease; } } // namespace ASFW::Async From d7da94535225f2acf01be7a6fe40af7ecde4f0e0 Mon Sep 17 00:00:00 2001 From: gly11 Date: Sun, 31 May 2026 19:09:52 +0800 Subject: [PATCH 21/22] fix(service): clear packet handlers before protocol teardown --- ASFWDriver/Service/DriverContext.cpp | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/ASFWDriver/Service/DriverContext.cpp b/ASFWDriver/Service/DriverContext.cpp index cf30695b..579d2642 100644 --- a/ASFWDriver/Service/DriverContext.cpp +++ b/ASFWDriver/Service/DriverContext.cpp @@ -10,6 +10,7 @@ #include "../Async/Interfaces/IFireWireBus.hpp" #include "../Async/PacketHelpers.hpp" #include "../Async/ResponseCode.hpp" +#include "../Async/Rx/PacketRouter.hpp" #include "../Async/Tx/ResponseSender.hpp" #include "../Audio/AudioCoordinator.hpp" #include "../Bus/BusManager.hpp" @@ -33,8 +34,24 @@ #include "../Protocols/SBP2/SBP2SessionRegistry.hpp" #include "../Scheduling/Scheduler.hpp" +namespace { + +void StopAndClearAsyncRouting(ServiceContext& ctx) { + if (!ctx.deps.asyncSubsystem) { + return; + } + + ctx.deps.asyncSubsystem->Stop(); + if (auto* router = ctx.deps.asyncSubsystem->GetPacketRouter()) { + router->ClearAllHandlers(); + } +} + +} // namespace + void ServiceContext::Reset() { stopping.store(true, std::memory_order_release); + StopAndClearAsyncRouting(*this); controller.reset(); audioCoordinator.reset(); deps.hardware.reset(); @@ -54,7 +71,7 @@ void ServiceContext::Reset() { deps.avcDiscovery.reset(); // Clean up AV/C discovery deps.irmClient.reset(); // Clean up IRM client deps.asyncController.reset(); - deps.asyncSubsystem.reset(); // Stop and cleanup asyncSubsystem + deps.asyncSubsystem.reset(); // Cleanup asyncSubsystem after handlers are cleared. statusPublisher.Reset(); watchdog.Reset(); #ifndef ASFW_HOST_TEST @@ -385,11 +402,9 @@ void DriverWiring::CleanupStartFailure(::ServiceContext& ctx) { ctx.controller.reset(); } - // CRITICAL: Stop asyncSubsystem BEFORE cancelling watchdog - // This prevents the crash where watchdog fires after completion queue is deactivated - if (ctx.deps.asyncSubsystem) { - ctx.deps.asyncSubsystem->Stop(); - } + // CRITICAL: Stop async routing before releasing protocol dependencies. + // This prevents PacketRouter callbacks from outliving captured SBP2/FCP objects. + StopAndClearAsyncRouting(ctx); if (ctx.deps.interrupts) ctx.deps.interrupts->Disable(); From 5dcbd8238d095f8e2b106b84466a26c0edbd00e1 Mon Sep 17 00:00:00 2001 From: gly11 Date: Sun, 31 May 2026 19:10:51 +0800 Subject: [PATCH 22/22] fix(service): use one block-write routing path --- ASFWDriver/ASFWDriver.cpp | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/ASFWDriver/ASFWDriver.cpp b/ASFWDriver/ASFWDriver.cpp index 5d7e6df9..33ef7e1a 100644 --- a/ASFWDriver/ASFWDriver.cpp +++ b/ASFWDriver/ASFWDriver.cpp @@ -35,8 +35,6 @@ #include "Async/AsyncSubsystem.hpp" #include "Async/DMAMemoryImpl.hpp" #include "Async/Interfaces/IFireWireBus.hpp" -#include "Async/PacketHelpers.hpp" -#include "Async/ResponseCode.hpp" #include "Audio/AudioCoordinator.hpp" #include "Bus/SelfIDCapture.hpp" #include "Common/DriverKitOwnership.hpp" @@ -224,32 +222,6 @@ kern_return_t IMPL(ASFWDriver, Start) { ASFW_LOG(Controller, "✅ FCPResponseRouter initialized"); } - if (ctx.deps.fcpResponseRouter && ctx.deps.asyncSubsystem) { - if (auto* router = ctx.deps.asyncSubsystem->GetPacketRouter()) { - router->RegisterRequestHandler( - 0x1, // tCode for Block Write Request - [fcpRouter = - ctx.deps.fcpResponseRouter.get()](const ASFW::Async::ARPacketView& packet) { - if (fcpRouter) { - const ASFW::Protocols::Ports::BlockWriteRequestView request{ - .sourceID = packet.sourceID, - .destOffset = ASFW::Async::ExtractDestOffset(packet.header), - .payload = packet.payload, - }; - - const auto disposition = fcpRouter->RouteBlockWrite(request); - if (disposition == - ASFW::Protocols::Ports::BlockWriteDisposition::kAddressError) { - return ASFW::Async::ResponseCode::AddressError; - } - return ASFW::Async::ResponseCode::Complete; - } - return ASFW::Async::ResponseCode::NoResponse; - }); - ASFW_LOG(Controller, "✅ FCPResponseRouter wired to PacketRouter (tCode 0x1)"); - } - } - DriverWiring::EnsureSbp2Deps(ctx); if (ctx.deps.speedPolicy) {