Austin Schuh | faec51a | 2023-09-08 17:43:32 -0700 | [diff] [blame^] | 1 | #ifndef AOS_IPC_LIB_LOCKLESS_QUEUE_STEPPING_H_ |
| 2 | #define AOS_IPC_LIB_LOCKLESS_QUEUE_STEPPING_H_ |
| 3 | |
| 4 | #include <cinttypes> |
| 5 | #include <functional> |
| 6 | |
| 7 | #include "aos/ipc_lib/lockless_queue.h" |
| 8 | #include "aos/ipc_lib/lockless_queue_memory.h" |
| 9 | |
| 10 | namespace aos { |
| 11 | namespace ipc_lib { |
| 12 | namespace testing { |
| 13 | |
| 14 | #if defined(__ARM_EABI__) |
| 15 | // There are various reasons why we might not actually be able to do this |
| 16 | // testing, but we still want to run the functions to test anything they test |
| 17 | // other than shm robustness. |
| 18 | // |
| 19 | // ARM doesn't have a nice way to do single-stepping, and I don't feel like |
| 20 | // dealing with editing the next instruction to be a breakpoint and then |
| 21 | // changing it back. |
| 22 | #else |
| 23 | |
| 24 | #define SUPPORTS_SHM_ROBUSTNESS_TEST |
| 25 | |
| 26 | // This code currently supports amd64 only, but it |
| 27 | // shouldn't be hard to port to i386 (it should just be using the name for |
| 28 | // only the smaller part of the flags register), but that's not tested, and |
| 29 | // porting it to any other architecture is more work. |
| 30 | // Currently, we skip doing anything exciting on arm (just run the code without |
| 31 | // any robustness testing) and refuse to compile anywhere else. |
| 32 | |
| 33 | #define SIMPLE_ASSERT(condition, message) \ |
| 34 | do { \ |
| 35 | if (!(condition)) { \ |
| 36 | static const char kMessage[] = message "\n"; \ |
| 37 | if (write(STDERR_FILENO, kMessage, sizeof(kMessage) - 1) != \ |
| 38 | (sizeof(kMessage) - 1)) { \ |
| 39 | static const char kFailureMessage[] = "writing failed\n"; \ |
| 40 | __attribute__((unused)) int ignore = write( \ |
| 41 | STDERR_FILENO, kFailureMessage, sizeof(kFailureMessage) - 1); \ |
| 42 | } \ |
| 43 | abort(); \ |
| 44 | } \ |
| 45 | } while (false) |
| 46 | |
| 47 | // Array to track writes to memory, and make sure they happen in the right |
| 48 | // order. |
| 49 | class WritesArray { |
| 50 | public: |
| 51 | uintptr_t At(size_t location) const { |
| 52 | SIMPLE_ASSERT(location < size_, "too far into writes array"); |
| 53 | return writes_[location]; |
| 54 | } |
| 55 | void Add(uintptr_t pointer) { |
| 56 | SIMPLE_ASSERT(size_ < kSize, "too many writes"); |
| 57 | writes_[size_++] = pointer; |
| 58 | } |
| 59 | |
| 60 | size_t size() const { return size_; } |
| 61 | |
| 62 | private: |
| 63 | static const size_t kSize = 20000; |
| 64 | |
| 65 | uintptr_t writes_[kSize]; |
| 66 | size_t size_ = 0; |
| 67 | }; |
| 68 | |
| 69 | enum class DieAtState { |
| 70 | // No SEGVs should be happening. |
| 71 | kDisabled, |
| 72 | // SEGVs are fine. Normal operation. |
| 73 | kRunning, |
| 74 | // We are manipulating a mutex. No SEGVs should be happening. |
| 75 | kWriting, |
| 76 | }; |
| 77 | |
| 78 | // What we exit with when we're exiting in the middle. |
| 79 | const int kExitEarlyValue = 123; |
| 80 | |
| 81 | // We have to keep track of everything in a global variable because there's no |
| 82 | // other way for the signal handlers to find it. |
| 83 | struct GlobalState { |
| 84 | // Constructs the singleton GlobalState. |
| 85 | static std::tuple<GlobalState *, WritesArray *> MakeGlobalState(); |
| 86 | |
| 87 | // Returns the global state. Atomic and safe from signal handlers. |
| 88 | static GlobalState *Get(); |
| 89 | |
| 90 | // Pointer to the queue memory, and its size. |
| 91 | void *lockless_queue_memory; |
| 92 | size_t lockless_queue_memory_size; |
| 93 | |
| 94 | // Pointer to a second block of memory the same size. This (on purpose) has |
| 95 | // the same size as lockless_queue_memory so we can point the robust mutexes |
| 96 | // here. |
| 97 | void *lockless_queue_memory_lock_backup; |
| 98 | |
| 99 | // Expected writes. |
| 100 | const WritesArray *writes_in; |
| 101 | // Actual writes. |
| 102 | WritesArray *writes_out; |
| 103 | // Location to die at, and how far we have gotten. |
| 104 | size_t die_at, current_location; |
| 105 | // State. |
| 106 | DieAtState state; |
| 107 | |
| 108 | // Returns true if the address is in the queue memory chunk. |
| 109 | bool IsInLocklessQueueMemory(void *address); |
| 110 | |
| 111 | // Calls mprotect(2) for the entire shared memory region with the given prot. |
| 112 | void ShmProtectOrDie(int prot); |
| 113 | |
| 114 | // Checks a write into the queue and conditionally dies. Tracks the write. |
| 115 | void HandleWrite(void *address); |
| 116 | |
| 117 | // Registers the handlers required to trap writes. |
| 118 | void RegisterSegvAndTrapHandlers(); |
| 119 | }; |
| 120 | |
| 121 | void TestShmRobustness(const LocklessQueueConfiguration &config, |
| 122 | ::std::function<void(void *)> prepare, |
| 123 | ::std::function<void(void *)> function, |
| 124 | ::std::function<void(void *)> check); |
| 125 | |
| 126 | // Capture the tid in the child so we can tell if it died. Uses mmap so it |
| 127 | // works across the process boundary. |
| 128 | class SharedTid { |
| 129 | public: |
| 130 | SharedTid(); |
| 131 | ~SharedTid(); |
| 132 | |
| 133 | // Captures the tid. |
| 134 | void Set(); |
| 135 | |
| 136 | // Returns the captured tid. |
| 137 | pid_t Get(); |
| 138 | |
| 139 | private: |
| 140 | pid_t *tid_; |
| 141 | }; |
| 142 | |
| 143 | // Sets FUTEX_OWNER_DIED if the owner was tid. This fakes what the kernel does |
| 144 | // with a robust mutex. |
| 145 | bool PretendOwnerDied(aos_mutex *mutex, pid_t tid); |
| 146 | |
| 147 | #endif |
| 148 | |
| 149 | } // namespace testing |
| 150 | } // namespace ipc_lib |
| 151 | } // namespace aos |
| 152 | |
| 153 | #endif // AOS_IPC_LIB_LOCKLESS_QUEUE_STEPPING_H_ |