diff options
Diffstat (limited to 'pw_rpc/fuzz/public/pw_rpc/fuzz')
-rw-r--r-- | pw_rpc/fuzz/public/pw_rpc/fuzz/alarm_timer.h | 56 | ||||
-rw-r--r-- | pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h | 230 | ||||
-rw-r--r-- | pw_rpc/fuzz/public/pw_rpc/fuzz/engine.h | 339 |
3 files changed, 625 insertions, 0 deletions
diff --git a/pw_rpc/fuzz/public/pw_rpc/fuzz/alarm_timer.h b/pw_rpc/fuzz/public/pw_rpc/fuzz/alarm_timer.h new file mode 100644 index 000000000..9ccd7ce0b --- /dev/null +++ b/pw_rpc/fuzz/public/pw_rpc/fuzz/alarm_timer.h @@ -0,0 +1,56 @@ +// Copyright 2022 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +#pragma once + +#include "pw_chrono/system_clock.h" +#include "pw_chrono/system_timer.h" + +namespace pw::rpc::fuzz { + +/// Represents a timer that invokes a callback on timeout. Once started, it will +/// invoke the callback after a provided duration unless it is restarted, +/// canceled, or destroyed. +class AlarmTimer { + public: + AlarmTimer(chrono::SystemTimer::ExpiryCallback&& on_timeout) + : timer_(std::move(on_timeout)) {} + + chrono::SystemClock::duration timeout() const { return timeout_; } + + /// "Arms" the timer. The callback will be invoked if `timeout` elapses + /// without a call to `Restart`, `Cancel`, or the destructor. Calling `Start` + /// again restarts the timer, possibly with a different `timeout` value. + void Start(chrono::SystemClock::duration timeout) { + timeout_ = timeout; + Restart(); + } + + /// Restarts the timer. This is equivalent to calling `Start` with the same + /// `timeout` as passed previously. Does nothing if `Start` has not been + /// called. + void Restart() { + Cancel(); + timer_.InvokeAfter(timeout_); + } + + /// "Disarms" the timer. The callback will not be invoked unless `Start` is + /// called again. Does nothing if `Start` has not been called. + void Cancel() { timer_.Cancel(); } + + private: + chrono::SystemTimer timer_; + chrono::SystemClock::duration timeout_; +}; + +} // namespace pw::rpc::fuzz diff --git a/pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h b/pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h new file mode 100644 index 000000000..05a7633e6 --- /dev/null +++ b/pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h @@ -0,0 +1,230 @@ +// Copyright 2023 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +#pragma once + +/// Command line argument parsing. +/// +/// The objects defined below can be used to parse command line arguments of +/// different types. These objects are "just enough" defined for current use +/// cases, but the design is intended to be extensible as new types and traits +/// are needed. +/// +/// Example: +/// +/// Given a boolean flag "verbose", a numerical flag "runs", and a positional +/// "port" argument to be parsed, we can create a vector of parsers. In this +/// example, we modify the parsers during creation to set default values: +/// +/// @code +/// Vector<ArgParserVariant, 3> parsers = { +/// BoolParser("-v", "--verbose").set_default(false), +/// UnsignedParser<size_t>("-r", "--runs").set_default(1000), +/// UnsignedParser<uint16_t>("port").set_default(11111), +/// }; +/// @endcode +/// +/// With this vector, we can then parse command line arguments and extract +/// the values of arguments that were set, e.g.: +/// +/// @code +/// if (!ParseArgs(parsers, argc, argv).ok()) { +/// PrintUsage(parsers, argv[0]); +/// return 1; +/// } +/// bool verbose; +/// size_t runs; +/// uint16_t port; +/// if (!GetArg(parsers, "--verbose", &verbose).ok() || +/// !GetArg(parsers, "--runs", &runs).ok() || +/// !GetArg(parsers, "port", &port).ok()) { +/// // Shouldn't happen unless names do not match. +/// return 1; +/// } +/// +/// // Do stuff with `verbose`, `runs`, and `port`... +/// @endcode + +#include <cstddef> +#include <cstdint> +#include <string_view> +#include <variant> + +#include "pw_containers/vector.h" +#include "pw_status/status.h" + +namespace pw::rpc::fuzz { + +/// Enumerates the results of trying to parse a specific command line argument +/// with a particular parsers. +enum ParseStatus { + /// The argument matched the parser and was successfully parsed without a + /// value. + kParsedOne, + + /// The argument matched the parser and was successfully parsed with a value. + kParsedTwo, + + /// The argument did not match the parser. This is not necessarily an error; + /// the argument may match a different parser. + kParseMismatch, + + /// The argument matched a parser, but could not be parsed. This may be due to + /// a missing value for a flag, a value of the wrong type, a provided value + /// being out of range, etc. Parsers should log additional details before + /// returning this value. + kParseFailure, +}; + +/// Holds parsed argument values of different types. +using ArgVariant = std::variant<std::monostate, bool, uint64_t>; + +/// Base class for argument parsers. +class ArgParserBase { + public: + virtual ~ArgParserBase() = default; + + std::string_view short_name() const { return short_name_; } + std::string_view long_name() const { return long_name_; } + bool positional() const { return positional_; } + + /// Clears the value. Typically, command line arguments are only parsed once, + /// but this method is useful for testing. + void Reset() { value_ = std::monostate(); } + + protected: + /// Defines an argument parser with a single name. This may be a positional + /// argument or a flag. + ArgParserBase(std::string_view name); + + /// Defines an argument parser for a flag with short and long names. + ArgParserBase(std::string_view shortopt, std::string_view longopt); + + void set_initial(ArgVariant initial) { initial_ = initial; } + void set_value(ArgVariant value) { value_ = value; } + + /// Examines if the given `arg` matches this parser. A parser for a flag can + /// match the short name (e.g. '-f') if set, or the long name (e.g. '--foo'). + /// A parser for a positional argument will match anything until it has a + /// value set. + bool Match(std::string_view arg); + + /// Returns the parsed value. + template <typename T> + T Get() const { + return std::get<T>(GetValue()); + } + + private: + const ArgVariant& GetValue() const; + + std::string_view short_name_; + std::string_view long_name_; + bool positional_; + + ArgVariant initial_; + ArgVariant value_; +}; + +// Argument parsers for boolean arguments. These arguments are always flags, and +// can be specified as, e.g. "-f" (true), "--foo" (true) or "--no-foo" (false). +class BoolParser : public ArgParserBase { + public: + BoolParser(std::string_view optname); + BoolParser(std::string_view shortopt, std::string_view longopt); + + bool value() const { return Get<bool>(); } + BoolParser& set_default(bool value); + + ParseStatus Parse(std::string_view arg0, + std::string_view arg1 = std::string_view()); +}; + +// Type-erasing argument parser for unsigned integer arguments. This object +// always parses values as `uint64_t`s and should not be used directly. +// Instead, use `UnsignedParser<T>` with a type to explicitly narrow to. +class UnsignedParserBase : public ArgParserBase { + protected: + UnsignedParserBase(std::string_view name); + UnsignedParserBase(std::string_view shortopt, std::string_view longopt); + + ParseStatus Parse(std::string_view arg0, std::string_view arg1, uint64_t max); +}; + +// Argument parser for unsigned integer arguments. These arguments may be flags +// or positional arguments. +template <typename T, typename std::enable_if_t<std::is_unsigned_v<T>, int> = 0> +class UnsignedParser : public UnsignedParserBase { + public: + UnsignedParser(std::string_view name) : UnsignedParserBase(name) {} + UnsignedParser(std::string_view shortopt, std::string_view longopt) + : UnsignedParserBase(shortopt, longopt) {} + + T value() const { return static_cast<T>(Get<uint64_t>()); } + + UnsignedParser& set_default(T value) { + set_initial(static_cast<uint64_t>(value)); + return *this; + } + + ParseStatus Parse(std::string_view arg0, + std::string_view arg1 = std::string_view()) { + return UnsignedParserBase::Parse(arg0, arg1, std::numeric_limits<T>::max()); + } +}; + +// Holds argument parsers of different types. +using ArgParserVariant = + std::variant<BoolParser, UnsignedParser<uint16_t>, UnsignedParser<size_t>>; + +// Parses the command line arguments and sets the values of the given `parsers`. +Status ParseArgs(Vector<ArgParserVariant>& parsers, int argc, char** argv); + +// Logs a usage message based on the given `parsers` and the program name given +// by `argv0`. +void PrintUsage(const Vector<ArgParserVariant>& parsers, + std::string_view argv0); + +// Attempts to find the parser in `parsers` with the given `name`, and returns +// its value if found. +std::optional<ArgVariant> GetArg(const Vector<ArgParserVariant>& parsers, + std::string_view name); + +inline void GetArgValue(const ArgVariant& arg, bool* out) { + *out = std::get<bool>(arg); +} + +template <typename T, typename std::enable_if_t<std::is_unsigned_v<T>, int> = 0> +void GetArgValue(const ArgVariant& arg, T* out) { + *out = static_cast<T>(std::get<uint64_t>(arg)); +} + +// Like `GetArgVariant` above, but extracts the typed value from the variant +// into `out`. Returns an error if no parser exists in `parsers` with the given +// `name`. +template <typename T> +Status GetArg(const Vector<ArgParserVariant>& parsers, + std::string_view name, + T* out) { + const auto& arg = GetArg(parsers, name); + if (!arg.has_value()) { + return Status::InvalidArgument(); + } + GetArgValue(*arg, out); + return OkStatus(); +} + +// Resets the parser with the given name. Returns an error if not found. +Status ResetArg(Vector<ArgParserVariant>& parsers, std::string_view name); + +} // namespace pw::rpc::fuzz diff --git a/pw_rpc/fuzz/public/pw_rpc/fuzz/engine.h b/pw_rpc/fuzz/public/pw_rpc/fuzz/engine.h new file mode 100644 index 000000000..34e92c003 --- /dev/null +++ b/pw_rpc/fuzz/public/pw_rpc/fuzz/engine.h @@ -0,0 +1,339 @@ +// Copyright 2023 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +#pragma once + +#include <atomic> +#include <cstdarg> +#include <cstddef> +#include <cstdint> +#include <thread> +#include <variant> + +#include "pw_containers/vector.h" +#include "pw_random/xor_shift.h" +#include "pw_rpc/benchmark.h" +#include "pw_rpc/benchmark.raw_rpc.pb.h" +#include "pw_rpc/fuzz/alarm_timer.h" +#include "pw_sync/condition_variable.h" +#include "pw_sync/lock_annotations.h" +#include "pw_sync/mutex.h" +#include "pw_sync/timed_mutex.h" + +namespace pw::rpc::fuzz { + +/// Describes an action a fuzzing thread can perform on a call. +struct Action { + enum Op : uint8_t { + /// No-op. + kSkip, + + /// Waits for the call indicated by `target` to complete. + kWait, + + /// Makes a new unary request using the call indicated by `target`. The data + /// written is derived from `value`. + kWriteUnary, + + /// Writes to a stream request using the call indicated by `target`, or + /// makes + /// a new one if not currently a stream call. The data written is derived + /// from `value`. + kWriteStream, + + /// Closes the stream if the call indicated by `target` is a stream call. + kCloseClientStream, + + /// Cancels the call indicated by `target`. + kCancel, + + /// Abandons the call indicated by `target`. + kAbandon, + + /// Swaps the call indicated by `target` with a call indicated by `value`. + kSwap, + + /// Sets the call indicated by `target` to an initial, unset state. + kDestroy, + }; + + constexpr Action() = default; + Action(uint32_t encoded); + Action(Op op, size_t target, uint16_t value); + Action(Op op, size_t target, char val, size_t len); + ~Action() = default; + + void set_thread_id(size_t thread_id_) { + thread_id = thread_id_; + callback_id = std::numeric_limits<size_t>::max(); + } + + void set_callback_id(size_t callback_id_) { + thread_id = 0; + callback_id = callback_id_; + } + + // For a write action's value, returns the character value to be written. + static char DecodeWriteValue(uint16_t value); + + // For a write action's value, returns the number of characters to be written. + static size_t DecodeWriteLength(uint16_t value); + + /// Returns a value that represents the fields of an action. Constructing an + /// `Action` with this value will produce the same fields. + uint32_t Encode() const; + + /// Records details of the action being performed if verbose logging is + /// enabled. + void Log(bool verbose, size_t num_actions, const char* fmt, ...) const; + + /// Records an encountered when trying to log an action. + void LogFailure(bool verbose, size_t num_actions, Status status) const; + + Op op = kSkip; + size_t target = 0; + uint16_t value = 0; + + size_t thread_id = 0; + size_t callback_id = std::numeric_limits<size_t>::max(); +}; + +/// Wraps an RPC call that may be either a `RawUnaryReceiver` or +/// `RawClientReaderWriter`. Allows applying `Action`s to each possible +/// type of call. +class FuzzyCall { + public: + using Variant = + std::variant<std::monostate, RawUnaryReceiver, RawClientReaderWriter>; + + explicit FuzzyCall(size_t index) : index_(index), id_(index) {} + ~FuzzyCall() = default; + + size_t id() { + std::lock_guard lock(mutex_); + return id_; + } + + bool pending() { + std::lock_guard lock(mutex_); + return pending_; + } + + /// Applies the given visitor to the call variant. If the action taken by the + /// visitor is expected to complete the call, it will notify any threads + /// waiting for the call to complete. This version of the method does not + /// return the result of the visiting the variant. + template <typename Visitor, + typename std::enable_if_t< + std::is_same_v<typename Visitor::result_type, void>, + int> = 0> + typename Visitor::result_type Visit(Visitor visitor, bool completes = true) { + { + std::lock_guard lock(mutex_); + std::visit(std::move(visitor), call_); + } + if (completes && pending_.exchange(false)) { + cv_.notify_all(); + } + } + + /// Applies the given visitor to the call variant. If the action taken by the + /// visitor is expected to complete the call, it will notify any threads + /// waiting for the call to complete. This version of the method returns the + /// result of the visiting the variant. + template <typename Visitor, + typename std::enable_if_t< + !std::is_same_v<typename Visitor::result_type, void>, + int> = 0> + typename Visitor::result_type Visit(Visitor visitor, bool completes = true) { + typename Visitor::result_type result; + { + std::lock_guard lock(mutex_); + result = std::visit(std::move(visitor), call_); + } + if (completes && pending_.exchange(false)) { + cv_.notify_all(); + } + return result; + } + + // Records the number of bytes written as part of a request. If `append` is + // true, treats the write as a continuation of a streaming request. + void RecordWrite(size_t num, bool append = false); + + /// Waits to be notified that a callback has been invoked. + void Await() PW_LOCKS_EXCLUDED(mutex_); + + /// Completes the call, notifying any waiters. + void Notify() PW_LOCKS_EXCLUDED(mutex_); + + /// Exchanges the call represented by this object with another. + void Swap(FuzzyCall& other); + + /// Resets the call wrapped by this object with a new one. Destorys the + /// previous call. + void Reset(Variant call = Variant()) PW_LOCKS_EXCLUDED(mutex_); + + // Reports the state of this object. + void Log() PW_LOCKS_EXCLUDED(mutex_); + + private: + /// This represents the index in the engine's list of calls. It is used to + /// ensure a consistent order of locking multiple calls. + const size_t index_; + + sync::TimedMutex mutex_; + sync::ConditionVariable cv_; + + /// An identifier that can be used find this object, e.g. by a callback, even + /// when it has been swapped with another call. + size_t id_ PW_GUARDED_BY(mutex_); + + /// Holds the actual pw::rpc::Call object, when present. + Variant call_ PW_GUARDED_BY(mutex_); + + /// Set when a request is sent, and cleared when a callback is invoked. + std::atomic_bool pending_ = false; + + /// Bytes sent in the last unary request or stream write. + size_t last_write_ PW_GUARDED_BY(mutex_) = 0; + + /// Total bytes sent using this call object. + size_t total_written_ PW_GUARDED_BY(mutex_) = 0; +}; + +/// The main RPC fuzzing engine. +/// +/// This class takes or generates a sequence of actions, and dsitributes them to +/// a number of threads that can perform them using an RPC client. Passing the +/// same seed to the engine at construction will allow it to generate the same +/// sequence of actions. +class Fuzzer { + public: + /// Number of fuzzing threads. The first thread counted is the RPC dispatch + /// thread. + static constexpr size_t kNumThreads = 4; + + /// Maximum number of actions that a single thread will try to perform before + /// exiting. + static constexpr size_t kMaxActionsPerThread = 255; + + /// The number of call objects available to be used for fuzzing. + static constexpr size_t kMaxConcurrentCalls = 8; + + /// The mxiumum number of individual fuzzing actions that the fuzzing threads + /// can perform. The `+ 1` is to allow the inclusion of a special `0` action + /// to separate each thread's actions when concatenated into a single list. + static constexpr size_t kMaxActions = + kNumThreads * (kMaxActionsPerThread + 1); + + explicit Fuzzer(Client& client, uint32_t channel_id); + + /// The fuzzer engine should remain pinned in memory since it is referenced by + /// the `CallbackContext`s. + Fuzzer(const Fuzzer&) = delete; + Fuzzer(Fuzzer&&) = delete; + Fuzzer& operator=(const Fuzzer&) = delete; + Fuzzer& operator=(Fuzzer&&) = delete; + + void set_verbose(bool verbose) { verbose_ = verbose; } + + /// Sets the timeout and starts the timer. + void set_timeout(chrono::SystemClock::duration timeout) { + timer_.Start(timeout); + } + + /// Generates encoded actions from the RNG and `Run`s them. + void Run(uint64_t seed, size_t num_actions); + + /// Splits the provided `actions` between the fuzzing threads and runs them to + /// completion. + void Run(const Vector<uint32_t>& actions); + + private: + /// Information passed to the RPC callbacks, including the index of the + /// associated call and a pointer to the fuzzer object. + struct CallbackContext { + size_t id; + Fuzzer* fuzzer; + }; + + /// Restarts the alarm timer, delaying it from detecting a timeout. This is + /// called whenever actions complete and indicates progress is still being + /// made. + void ResetTimerLocked() PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + /// Decodes the `encoded` action and performs it. The `thread_id` is used for + /// verbose diagnostics. When invoked from `PerformCallback` the `callback_id` + /// will be set to the index of the associated call. This allows avoiding + /// specific, prohibited actions, e.g. destroying a call from its own + /// callback. + void Perform(const Action& action) PW_LOCKS_EXCLUDED(mutex_); + + /// Returns the call with the matching `id`. + FuzzyCall& FindCall(size_t id) PW_LOCKS_EXCLUDED(mutex_) { + std::lock_guard lock(mutex_); + return FindCallLocked(id); + } + + FuzzyCall& FindCallLocked(size_t id) PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_) { + return fuzzy_calls_[indices_[id]]; + } + + /// Returns a pointer to callback context for the given call index. + CallbackContext* GetContext(size_t callback_id) PW_LOCKS_EXCLUDED(mutex_) { + std::lock_guard lock(mutex_); + return &contexts_[callback_id]; + } + + /// Callback for stream write made by the call with the given `callback_id`. + void OnNext(size_t callback_id) PW_LOCKS_EXCLUDED(mutex_); + + /// Callback for completed request for the call with the given `callback_id`. + void OnCompleted(size_t callback_id) PW_LOCKS_EXCLUDED(mutex_); + + /// Callback for an error for the call with the given `callback_id`. + void OnError(size_t callback_id, Status status) PW_LOCKS_EXCLUDED(mutex_); + + bool verbose_ = false; + pw_rpc::raw::Benchmark::Client client_; + BenchmarkService service_; + + /// Alarm thread that detects when no workers have made recent progress. + AlarmTimer timer_; + + sync::Mutex mutex_; + + /// Worker threads. The first thread is the RPC response dispatcher. + Vector<std::thread, kNumThreads> threads_; + + /// RPC call objects. + Vector<FuzzyCall, kMaxConcurrentCalls> fuzzy_calls_; + + /// Maps each call's IDs to its index. Since calls may be move before their + /// callbacks are invoked, this list can be used to find the original call. + Vector<size_t, kMaxConcurrentCalls> indices_ PW_GUARDED_BY(mutex_); + + /// Context objects used to reference the engine and call. + Vector<CallbackContext, kMaxConcurrentCalls> contexts_ PW_GUARDED_BY(mutex_); + + /// Set of actions performed as callbacks from other calls. + Vector<uint32_t, kMaxActionsPerThread> callback_actions_ + PW_GUARDED_BY(mutex_); + Vector<uint32_t>::iterator callback_iterator_ PW_GUARDED_BY(mutex_); + + /// Total actions performed by all workers. + std::atomic<size_t> num_actions_ = 0; +}; + +} // namespace pw::rpc::fuzz |