// Copyright (c) 2017-2025 Cloudflare, Inc. // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0 #pragma once // MSVC uses a different attribute name for no_unique_address #if _MSC_VER #define WD_NO_UNIQUE_ADDRESS [[msvc::no_unique_address]] #else #define WD_NO_UNIQUE_ADDRESS [[no_unique_address]] #endif // State Machine Abstraction built on kj::OneOf. // TODO(later): If this proves useful, consider moving it into kj itself as there // are no workerd-specific dependencies. // // Entire implementation was Claude-generated initially. // // Most of the detailed doc comments here are largely intended to be used by agents // and tooling. Human readers may prefer to just skip to the actual code. // // This header provides utilities for building type-safe state machines using kj::OneOf. // It addresses common patterns found throughout the workerd codebase with improvements // that provide tangible benefits over raw kj::OneOf usage. // // ============================================================================= // WHY USE THIS INSTEAD OF RAW kj::OneOf? // ============================================================================= // // Throughout workerd, we use kj::OneOf as a state machine to track the lifecycle // of streams, readers, writers, and other resources. A typical pattern looks like: // // kj::OneOf state; // // void read() { // KJ_SWITCH_ONEOF(state) { // KJ_CASE_ONEOF(readable, Readable) { // auto data = readable.source->read(); // Get reference to state // processData(data); // Call some function... // readable.source->advance(); // Use reference again - UAF! // } // KJ_CASE_ONEOF(closed, Closed) { ... } // KJ_CASE_ONEOF(err, kj::Exception) { ... } // } // } // // THE PROBLEM: Use-After-Free (UAF) from unsound state-transitions // // The `readable` reference points into the kj::OneOf's internal storage. If ANY // code path between obtaining that reference and using it triggers a state // transition (even indirectly through callbacks, promise continuations, or // nested calls), the reference becomes dangling: // // KJ_CASE_ONEOF(readable, Readable) { // readable.source->read(); // This might call back into our code... // // ...which might call close()... // // ...which does state.init() // readable.buffer.size(); // UAF! readable is now destroyed // } // // This is particularly insidious because: // 1. The bug may not manifest in simple tests // 2. It depends on complex callback chains that are hard to reason about // 3. It causes memory corruption that may crash much later // 4. ASAN/valgrind may not catch it if the memory is quickly reused // // HOW StateMachine HELPS: // // 1. TRANSITION LOCKING via whenState()/whenActive(): // // state.whenState([](Readable& r) { // r.source->read(); // If this tries to transition... // r.buffer.size(); // ...it throws instead of UAF // }); // // The callback holds a "transition lock" - any attempt to transition the // state machine while the lock is held will throw an exception instead of // silently corrupting memory. This turns silent UAF into a loud, debuggable // failure. // // 2. DEFERRED TRANSITIONS for async operations: // // When code legitimately needs to transition during an operation (e.g., // a read discovers EOF and needs to close), use deferred transitions: // // { // auto op = state.scopedOperation(); // state.whenActive([&](Readable& r) { // if (r.source->atEof()) { // state.deferTransitionTo(); // Queued, not immediate // } // }); // } // Transition happens here, after callback completes safely // // 3. TERMINAL STATE ENFORCEMENT: // // Once a stream is Closed or Errored, it should never transition back to // Readable. Raw kj::OneOf allows this silently: // // state.init(); // state.init(...); // Oops - zombie stream! // // StateMachine with TerminalStates<> will throw if you attempt this, // catching the bug immediately. // // 4. SEMANTIC HELPERS: // // Instead of: state.is() || state.is() // Write: state.isTerminal() or state.isInactive() // // Instead of: KJ_IF_SOME(e, state.tryGetUnsafe()) { ... } // Write: KJ_IF_SOME(e, state.tryGetErrorUnsafe()) { ... } // // WHEN TO USE: // // - Simple state tracking: StateMachine is fine // - Resource lifecycle (streams, handles): Use TerminalStates + PendingStates // - Migrating existing code: See MIGRATION GUIDE section below // // ============================================================================= // STATE MACHINE // ============================================================================= // // StateMachine supports composable features via spec types: // // // Simple (no specs) // StateMachine basic; // // // With terminal state enforcement // StateMachine, Idle, Running, Done> withTerminal; // // // With error extraction helpers // StateMachine, Active, Closed, Errored> withError; // // // With deferred transitions // StateMachine, Active, Closed, Errored> withDefer; // // // Full-featured (combine any specs) // StateMachine< // TerminalStates, // ErrorState, // ActiveState, // PendingStates, // Active, Closed, Errored // > fullyFeatured; // // Available spec types: // - TerminalStates - States that cannot be transitioned FROM // Enables: isTerminal() // - ErrorState - Designates the error state type // Enables: isErrored(), tryGetErrorUnsafe(), getErrorUnsafe() // - ActiveState - Designates the active/working state type // Enables: isActive(), isInactive(), whenActive(), whenActiveOr(), // tryGetActiveUnsafe(), requireActiveUnsafe() // - PendingStates - States that can be deferred during operations // Enables: beginOperation(), endOperation(), deferTransitionTo(), etc. // // NAMING CONVENTIONS: // - isTerminal() = current state is in TerminalStates (enforces no outgoing transitions) // - isInactive() = current state is NOT the ActiveState (semantic "done" state) // // ============================================================================= // MEMORY SAFETY // ============================================================================= // // THREAD SAFETY: State machines are NOT thread-safe. All operations on a // single state machine instance must be performed from the same thread. // If you need concurrent access, use external synchronization. // // This utility provides protections against common memory safety issues: // // 1. TRANSITION LOCKING: The state machine can be locked during callbacks to // prevent transitions that would invalidate references: // // machine.whenState([](Active& a) { // // machine.transitionTo(); // Would fail - locked! // a.resource->read(); // Safe - Active cannot be destroyed // }); // // 2. TRANSITION LOCK ENFORCEMENT: The machine tracks active transition locks // and throws if a transition is attempted while locks are held. // // 3. SAFE ACCESS PATTERNS: Prefer whenState() and whenActive() over get() // to ensure references don't outlive their validity. // // UNSAFE PATTERNS TO AVOID: // // // DON'T: Store references from getUnsafe() across transitions // Active& active = machine.getUnsafe(); // machine.transitionTo(); // active is now dangling! // // // DO: Use whenState() for safe scoped access // machine.whenState([](Active& a) { // // a is guaranteed valid for the duration of the callback // }); // // // DON'T: Transition inside a callback (will fail if locked) // machine.whenState([&](Active& a) { // machine.transitionTo(); // Fails! // }); // // // DO: Return a value and transition after // auto result = machine.whenState([](Active& a) { // return a.computeSomething(); // }); // machine.transitionTo(); // // ============================================================================= // QUICK START // ============================================================================= // // Define your state types (add NAME for introspection): // // struct Readable { // static constexpr kj::StringPtr NAME = "readable"_kj; // kj::Own source; // }; // struct Closed { static constexpr kj::StringPtr NAME = "closed"_kj; }; // struct Errored { // static constexpr kj::StringPtr NAME = "errored"_kj; // jsg::Value error; // }; // // Basic state machine with safe access: // // StateMachine state; // state.transitionTo(...); // // // RECOMMENDED: Use whenState() for safe scoped access // state.whenState([](Readable& r) { // r.source->read(); // Safe - transitions blocked during callback // }); // // // Or with a return value // auto size = state.whenState([](Readable& r) { // return r.source->size(); // }); // Returns kj::Maybe // // Stream-like state machine (common pattern in workerd): // // StateMachine< // TerminalStates, // ErrorState, // ActiveState, // PendingStates, // Readable, Closed, Errored // > state; // // state.transitionTo(...); // // // Safe access with whenActive() // state.whenActive([](Readable& r) { // r.source->doSomething(); // Transitions blocked // }); // // // Error checking // if (state.isErrored()) { ... } // KJ_IF_SOME(err, state.tryGetErrorUnsafe()) { ... } // // // Deferred transitions during operations // state.beginOperation(); // state.deferTransitionTo(); // Deferred until operation ends // state.endOperation(); // Now transitions to Closed // // // Terminal enforcement // state.transitionTo(); // state.transitionTo(...); // FAILS - can't leave terminal state // // ============================================================================= // MIGRATION GUIDE: From kj::OneOf to StateMachine // ============================================================================= // // This section describes how to migrate existing kj::OneOf state management // to use these StateMachine utilities. // // STEP 1: Add NAME constants to state types // ----------------------------------------- // StateMachine provides currentStateName() for debugging. Add NAME to states: // // // Before: // struct Closed {}; // // // After: // struct Closed { // static constexpr kj::StringPtr NAME = "Closed"_kj; // }; // // STEP 2: Replace kj::OneOf with appropriate StateMachine // -------------------------------------------------------- // // // Before: // kj::OneOf state; // // // After (basic): // StateMachine state; // // // After (with features): // StateMachine< // TerminalStates, // ErrorState, // ActiveState, // Closed, Errored, Readable // > state; // // STEP 3: Update state assignments to use transitionTo() // ------------------------------------------------------ // // // Before: // state = Closed{}; // state = Errored{kj::mv(error)}; // // // After: // state.transitionTo(); // state.transitionTo(kj::mv(error)); // // STEP 4: Update state checks // --------------------------- // // // Before: // if (state.is() || state.is()) { ... } // if (state.is()) { ... } // // // After (with ActiveState): // if (state.isInactive()) { ... } // Not in active state // // // After (with ErrorState): // if (state.isErrored()) { ... } // // STEP 5: Replace unsafe get() with safe access patterns // ------------------------------------------------------ // // // Before (unsafe - reference may dangle if callback transitions): // KJ_SWITCH_ONEOF(state) { // KJ_CASE_ONEOF(readable, Readable) { // readable.source->read(); // May be unsafe // } // } // // // After (safe - transitions blocked during callback): // state.whenActive([](Readable& r) { // r.source->read(); // Safe // }); // // // Or for specific state: // state.whenState([](Readable& r) { // r.source->read(); // }); // // STEP 6: Replace manual deferred-transition bookkeeping // ------------------------------------------------------ // If you have code that tracks pending operations and defers close/error: // // // Before: // bool closing = false; // int pendingOps = 0; // // void startOp() { pendingOps++; } // void endOp() { // if (--pendingOps == 0 && closing) doClose(); // } // void close() { // if (pendingOps > 0) { closing = true; return; } // doClose(); // } // // // After (with PendingStates): // void startOp() { state.beginOperation(); } // void endOp() { state.endOperation(); } // Auto-applies pending // void close() { state.deferTransitionTo(); } // // // Or with RAII: // void doWork() { // auto op = state.scopedOperation(); // // ... work ... // } // endOperation() called automatically // // STEP 7: Update visitForGc // ------------------------- // // // Before: // void visitForGc(jsg::GcVisitor& visitor) { // KJ_SWITCH_ONEOF(state) { // KJ_CASE_ONEOF(e, Errored) { visitor.visit(e.reason); } // // ... // } // } // // // After: // void visitForGc(jsg::GcVisitor& visitor) { // state.visitForGc(visitor); // Visits all GC-able states automatically // } // // STEP 8: KJ_SWITCH_ONEOF still works // ----------------------------------- // If you need to keep KJ_SWITCH_ONEOF for complex logic: // // KJ_SWITCH_ONEOF(state.underlying()) { // KJ_CASE_ONEOF(r, Readable) { ... } // KJ_CASE_ONEOF(c, Closed) { ... } // KJ_CASE_ONEOF(e, Errored) { ... } // } // // Or use the visitor pattern: // // state.visit([](auto& s) { // using S = kj::Decay; // if constexpr (kj::isSameType()) { ... } // else if constexpr (kj::isSameType()) { ... } // else { ... } // }); // // ============================================================================= #include #include #include #include #include #include #include #include namespace workerd { // ============================================================================= // Type Traits and Helpers // ============================================================================= namespace _ { // private // Helper to check if a type is in a parameter pack template inline constexpr bool isOneOf = false; template inline constexpr bool isOneOf = kj::isSameType() || isOneOf; // Concept: type has a static NAME member of type kj::StringPtr template concept HasStateName = requires { { T::NAME } -> std::convertible_to; }; // Get state name, using NAME if available, otherwise a placeholder template constexpr kj::StringPtr getStateName() { if constexpr (HasStateName) { return T::NAME; } else { return "(unnamed)"_kj; } } } // namespace _ // ============================================================================= // Spec Types for Composable Features // ============================================================================= // Marker type to specify terminal states (cannot transition FROM these) template struct TerminalStates { template static constexpr bool contains = _::isOneOf; template static bool isTerminal(const Machine& machine) { return (machine.template is() || ...); } }; // Marker type to specify the error state (enables isErrored(), tryGetErrorUnsafe(), etc.) // Note: Error states are implicitly terminal - you cannot transition out of an error state // using normal transitions. Use forceTransitionTo() if you need to reset from an error. template struct ErrorState { using Type = T; }; // Marker type to specify the active state (enables isActive(), whenActive(), etc.) template struct ActiveState { using Type = T; }; // Marker type to specify which states can be pending/deferred template struct PendingStates { template static constexpr bool contains = _::isOneOf; }; // ============================================================================= // Spec Detection Traits // ============================================================================= namespace _ { // private // Helper to detect template instantiations template class Template> inline constexpr bool isInstanceOf = false; template