// Copyright Epic Games, Inc. All Rights Reserved. #if (defined(__AUTORTFM) && __AUTORTFM) #include "Transaction.h" #include "TransactionInlines.h" #include "CallNestInlines.h" #include "ContextStatus.h" #include "HitSet.h" #include "Utils.h" #include #include namespace AutoRTFM { void FTransaction::Resurrect(FContext* const InContext) { AUTORTFM_ASSERT(Context == InContext) Parent = nullptr; StatDepth = 1; RecordedWriteHash = 0; NumWriteLogsHashed = 0; CurrentMemoryValidationLevel = EMemoryValidationLevel::Disabled; CurrentOpenReturnAddress = nullptr; CurrentState = EState::Uninitialized; bIsStackScoped = false; bIsInAllocateFn = false; } void FTransaction::Suppress() { CurrentState = EState::Done; Reset(); } FTransaction** FTransaction::GetIntrusiveAddress() { return &Parent; } FTransaction::FTransaction(FContext* Context) : Context(Context) , CommitTasks(Context->GetTaskPool()) , AbortTasks(Context->GetTaskPool()) { Resurrect(Context); } FTransaction::~FTransaction() { Suppress(); } void FTransaction::Initialize(FTransaction* Parent_, bool bIsStackScoped_, FStackRange StackRange_) { AUTORTFM_ASSERT(CurrentState == EState::Uninitialized); Parent = Parent_; bIsStackScoped = bIsStackScoped_; StackRange = StackRange_; // For stats, record the nested depth of the transaction. if (Parent) { StatDepth = Parent->StatDepth + 1; } Stats.Collect(StatDepth); Stats.Collect(StatDepth); } void FTransaction::Reset() { AUTORTFM_ASSERT(IsDone()); CommitTasks.Reset(); AbortTasks.Reset(); HitSet.Reset(); NewMemoryTracker.Reset(); WriteLog.Reset(); CurrentMemoryValidationLevel = EMemoryValidationLevel::Disabled; // Reset to the initial state. CurrentState = EState::Uninitialized; DeferredPopOnCommitHandlers.Reset(); DeferredPopOnAbortHandlers.Reset(); DeferredPopAllOnCommitHandlers.Reset(); DeferredPopAllOnAbortHandlers.Reset(); AUTORTFM_ASSERT(IsFresh()); } bool FTransaction::IsFresh() const { return HitSet.IsEmpty() && NewMemoryTracker.IsEmpty() && WriteLog.IsEmpty() && CommitTasks.IsEmpty() && AbortTasks.IsEmpty() && !IsDone() && DeferredPopOnCommitHandlers.IsEmpty() && DeferredPopOnAbortHandlers.IsEmpty() && DeferredPopAllOnCommitHandlers.IsEmpty() && DeferredPopAllOnAbortHandlers.IsEmpty(); } void FTransaction::AbortWithoutThrowing() { AUTORTFM_VERBOSE("Aborting '%hs'!", GetContextStatusName(Context->GetStatus())); AUTORTFM_ASSERT(Context->IsAborting()); Stats.Collect(); CollectStats(); // Ensure that we enter the done state before applying the commit, as this // will ensure the open-memory validation is performed before the write log // is cleared. SetDone(); // Call the destructors of all the OnCommit functors before undoing the transactional memory and // calling the OnAbort callbacks. This is important as the callback functions may have captured // variables that are depending on the allocated memory. CommitTasks.Reset(); Undo(); AbortTasks.RemoveEachBackward([&](TTask& Task) { Task(); }); if (IsNested()) { AUTORTFM_ASSERT(Parent); } else { AUTORTFM_ASSERT(Context->IsAborting()); } } void FTransaction::AbortAndThrow() { AbortWithoutThrowing(); Context->Throw(); } bool FTransaction::AttemptToCommit() { AUTORTFM_ASSERT(Context->GetStatus() == EContextStatus::Committing); AUTORTFM_ASSERT(Context->GetCurrentTransaction() == this); Stats.Collect(); CollectStats(); // Ensure that we enter the done state before applying the commit, as this // will ensure the open-memory validation is performed before the write log // is cleared. SetDone(); bool bResult; if (IsNested()) { CommitNested(); bResult = true; } else { bResult = AttemptToCommitOuterNest(); } return bResult; } void FTransaction::Undo() { AUTORTFM_VERBOSE("Undoing a transaction..."); AUTORTFM_ASSERT(IsDone()); for(auto Iter = WriteLog.rbegin(); Iter != WriteLog.rend(); ++Iter) { FWriteLogEntry Entry = *Iter; // No write records should be within the transaction's stack range. AUTORTFM_ENSURE(!IsOnStack(Entry.LogicalAddress)); memcpy(Entry.LogicalAddress, Entry.Data, Entry.Size); } AUTORTFM_VERBOSE("Undone a transaction!"); } void FTransaction::CommitNested() { AUTORTFM_ASSERT(Parent); // We need to pass our write log to our parent transaction, but with care! // We need to discard any writes if the memory location is on the parent // transaction's stack range. for (FWriteLogEntry Write : WriteLog) { if (Parent->IsOnStack(Write.LogicalAddress)) { continue; } if (Write.Size <= FHitSet::MaxSize) { FHitSetEntry HitSetEntry{}; HitSetEntry.Address = reinterpret_cast(Write.LogicalAddress); HitSetEntry.Size = static_cast(Write.Size); HitSetEntry.bNoMemoryValidation = Write.bNoMemoryValidation; if (Parent->HitSet.FindOrTryInsert(HitSetEntry)) { continue; // Don't duplicate the write-log entry. } } Parent->WriteLog.Push(Write); } // For all the deferred calls to `PopOnCommitHandler` that we couldn't // process (because our transaction nest didn't `PushOnCommitHandler`) // we need to move these to the parent now to handle them. for (const void* Key : DeferredPopOnCommitHandlers) { Parent->PopDeferUntilCommitHandler(Key); } DeferredPopOnCommitHandlers.Reset(); // For all the deferred calls to `PopOnAbortHandler` that we couldn't // process (because our transaction nest didn't `PushOnAbortHandler`) // we need to move these to the parent now to handle them. for (const void* Key : DeferredPopOnAbortHandlers) { Parent->PopDeferUntilAbortHandler(Key); } DeferredPopOnAbortHandlers.Reset(); // For all the calls to `PopAllOnCommitHandlers` we need to run these // again on parent now to handle them there too. for (const void* Key : DeferredPopAllOnCommitHandlers) { Parent->PopAllDeferUntilCommitHandlers(Key); } DeferredPopAllOnCommitHandlers.Reset(); // For all the calls to `PopAllOnAbortHandlers` we need to run these // again on parent now to handle them there too. for (const void* Key : DeferredPopAllOnAbortHandlers) { Parent->PopAllDeferUntilAbortHandlers(Key); } DeferredPopAllOnAbortHandlers.Reset(); Parent->CommitTasks.AddAll(std::move(CommitTasks)); Parent->AbortTasks.AddAll(std::move(AbortTasks)); Parent->NewMemoryTracker.Merge(NewMemoryTracker); } bool FTransaction::AttemptToCommitOuterNest() { AUTORTFM_ASSERT(!Parent); AUTORTFM_VERBOSE("About to run commit tasks!"); Context->DumpState(); AUTORTFM_VERBOSE("Running commit tasks..."); AbortTasks.Reset(); CommitTasks.RemoveEachForward([] (TTask& Task) { Task(); }); return true; } void FTransaction::SetOpenActiveValidatorEnabled(EMemoryValidationLevel NewMemoryValidationLevel, const void* ReturnAddress) { AUTORTFM_ASSERT(NewMemoryValidationLevel != EMemoryValidationLevel::Disabled); CurrentMemoryValidationLevel = NewMemoryValidationLevel; CurrentOpenReturnAddress = ReturnAddress; if (AutoRTFM::ForTheRuntime::GetMemoryValidationThrottlingEnabled()) { FOpenHashThrottler& Throttler = Context->GetOpenHashThrottler(); if (!Throttler.ShouldHashFor(ReturnAddress)) { CurrentMemoryValidationLevel = EMemoryValidationLevel::Disabled; } Throttler.Update(); } SetState(); } void FTransaction::SetOpenActive(EMemoryValidationLevel NewMemoryValidationLevel, const void* ReturnAddress) { if (AUTORTFM_UNLIKELY(NewMemoryValidationLevel != EMemoryValidationLevel::Disabled)) { AUTORTFM_MUST_TAIL return SetOpenActiveValidatorEnabled(NewMemoryValidationLevel, ReturnAddress); } CurrentMemoryValidationLevel = NewMemoryValidationLevel; CurrentOpenReturnAddress = ReturnAddress; // TODO: Validate if open -> open with different validation levels. RecordedWriteHash = 0; NumWriteLogsHashed = 0; CurrentOpenReturnAddress = nullptr; SetState(); } void FTransaction::SetClosedActive() { SetState(); } void FTransaction::SetOpenInactive() { SetState(); } void FTransaction::SetClosedInactive() { SetState(); } void FTransaction::SetActive() { switch (CurrentState) { case EState::OpenActive: case EState::ClosedActive: break; case EState::OpenInactive: SetState(); break; case EState::ClosedInactive: SetState(); break; default: AUTORTFM_FATAL("Invalid state"); } } void FTransaction::SetInactive() { switch (CurrentState) { case EState::OpenInactive: case EState::ClosedInactive: break; case EState::OpenActive: SetState(); break; case EState::ClosedActive: SetState(); break; default: AUTORTFM_FATAL("Invalid state"); } } void FTransaction::SetDone() { SetState(); } template void FTransaction::SetState() { AUTORTFM_ASSERT(NewState != CurrentState); switch (CurrentState) { case EState::Uninitialized: AUTORTFM_ASSERT(NewState == EState::OpenActive || NewState == EState::ClosedActive); break; // OpenActive -> OpenInactive, ClosedActive or Done case EState::OpenActive: AUTORTFM_ASSERT(NewState == EState::OpenInactive || NewState == EState::ClosedActive || NewState == EState::Done); if (CurrentMemoryValidationLevel != EMemoryValidationLevel::Disabled) { ValidateWriteHash(); RecordedWriteHash = 0; NumWriteLogsHashed = 0; } else { AUTORTFM_ASSERT(RecordedWriteHash == 0 && NumWriteLogsHashed == 0); } break; // ClosedActive -> ClosedInactive, OpenActive or Done case EState::ClosedActive: AUTORTFM_ASSERT(NewState == EState::ClosedInactive || NewState == EState::OpenActive || NewState == EState::Done); break; // ClosedActive -> OpenActive case EState::OpenInactive: AUTORTFM_ASSERT(NewState == EState::OpenActive); break; // ClosedInactive -> ClosedActive case EState::ClosedInactive: AUTORTFM_ASSERT(NewState == EState::ClosedActive); break; case EState::Done: AUTORTFM_FATAL("Once Done, the transaction cannot change state without a call to Reset()"); break; default: AUTORTFM_FATAL("Invalid state"); break; } // OpenInactive, ClosedActive or Done -> OpenActive if (NewState == EState::OpenActive) { if (CurrentMemoryValidationLevel != EMemoryValidationLevel::Disabled) { AUTORTFM_ASSERT(RecordedWriteHash == 0 && NumWriteLogsHashed == 0); RecordWriteHash(); } } CurrentState = NewState; } void FTransaction::DebugBreakIfMemoryValidationFails() { if (CurrentMemoryValidationLevel != EMemoryValidationLevel::Disabled) { FWriteHash OldHash = RecordedWriteHash; FWriteHash NewHash = CalculateNestedWriteHash(); if (OldHash != NewHash) { AUTORTFM_WARN("DebugBreakIfInvalidMemoryHash() detected a change in hash"); __builtin_debugtrap(); } } } void FTransaction::RecordWriteHash() { NumWriteLogsHashed = WriteLog.Num(); RecordedWriteHash = CalculateNestedWriteHash(); Context->GetOpenHashThrottler().Update(); } void FTransaction::ValidateWriteHash() const { FWriteHash OldHash = RecordedWriteHash; FWriteHash NewHash = CalculateNestedWriteHash(); Context->GetOpenHashThrottler().Update(); static constexpr const char Message[] = "Memory modified in a transaction was also modified in an call to AutoRTFM::Open(). " "This may lead to memory corruption if the transaction is aborted."; if (AUTORTFM_UNLIKELY(OldHash != NewHash)) { if (CurrentMemoryValidationLevel == EMemoryValidationLevel::Warn) { AUTORTFM_WARN(Message); } else { if (!ForTheRuntime::GetEnsureOnInternalAbort()) { AUTORTFM_FATAL(Message); } else { AUTORTFM_ENSURE_MSG(OldHash == NewHash, Message); } } } } FTransaction::FWriteHash FTransaction::CalculateNestedWriteHash() const { return CalculateNestedWriteHashWithLimit(NumWriteLogsHashed, CurrentOpenReturnAddress); } FTransaction::FWriteHash FTransaction::CalculateNestedWriteHashWithLimit(size_t NumWriteEntries, const void *OpenReturnAddress) const { FWriteHash Hash = 0; if (nullptr != Parent) { Hash = 31 * Parent->CalculateNestedWriteHashWithLimit(Parent->WriteLog.Num(), OpenReturnAddress); } { FOpenHashThrottler::FHashScope Profile(Context->GetOpenHashThrottler(), OpenReturnAddress, WriteLog); Hash ^= WriteLog.Hash(NumWriteEntries); } return Hash; } } // namespace AutoRTFM #endif // defined(__AUTORTFM) && __AUTORTFM