// Copyright 2024 syzkaller project authors. All rights reserved. // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. // CoverFilter is PC hash set that can be placed in shared memory. // // The set can cover up to 4 distinct 1GB regions of PCs. // This restriction allows for efficient, simple and shared memory compatible representation, // but should be enough to cover any reasonable combination of kernel/modules mapping. // // Low 3 bits of PCs are discarded. This reduces memory consumption 8x, but allows for some false positives. // However, in practice false positives should be very rare. A typical coverage call instruction is 4/5 bytes, // and there must be at least 1 other instruction in between them to make them different basic blocks, // so it's practically impossible to place 2 of them in the same 8-byte region. // For signal with hashed low 12 bits the probability is also low b/c overall density of coverage callbacks // is relatively low, a KASAN Linux kernel contains 1 callback per 88 bytes of code on average. // So even if we discard low 3 bits, average densitiy is still 1/11. // For gVisor with dense coverage IDs special care must be taken to avoid collisions. // // The set is organized as a 3 level table. // The top "region" level is linear lookup, but contains at most 4 entries, each covering 1GB. // Most likely the first entry is the right one. This level allows to cover unconnected regions of PCs. // The next "L1" level splits 1GB chunks into 1MB chunks, and allows to allocate memory only // for a subset of these 1MB chunks. // The last "L2" level covers 1MB chunks with 16KB bitmaps (1MB divided by 8 for 3 discarded PC bits, // and divided by 8 again for 8 bits in a byte). class CoverFilter { public: CoverFilter() : shmem_(kMemSize), tab_(static_cast(shmem_.Mem())) { } CoverFilter(int fd, void* preferred = nullptr) : shmem_(fd, preferred, kMemSize, false), tab_(static_cast(shmem_.Mem())) { } void Insert(uint64 pc) { auto [byte, bit] = FindByte(pc, true); byte |= bit; } bool Contains(uint64 pc) { auto [byte, bit] = FindByte(pc, false); return byte & bit; } // Prevents any future modifications to the filter. void Seal() { shmem_.Seal(); } int FD() const { return shmem_.FD(); } private: static constexpr size_t kNumRegions = 4; static constexpr size_t kL1Size = 1 << 30; static constexpr size_t kL2Size = 1 << 20; static constexpr size_t kPCDivider = 8; static constexpr size_t kByteBits = 8; // Approximately how much .text we can cover (2GB of PCs require 32MB shmem region). static constexpr size_t kMaxCovered = 2ull << 30; static constexpr size_t kCompression = kPCDivider * kByteBits; static constexpr size_t kMemSize = kMaxCovered / kCompression; static constexpr size_t kNoRegion = static_cast(-1); struct Table { uint64 regions[kNumRegions]; uint16 l1[kNumRegions][kL1Size / kL2Size]; uint8 l2[][kL2Size / kCompression]; }; ShmemFile shmem_; Table* tab_ = nullptr; uint16 alloc_ = 0; std::pair FindByte(uint64 pc, bool add = false) { static const uint8 empty = 0; size_t reg = FindRegion(pc, add); if (reg == kNoRegion) return {const_cast(empty), 0}; size_t l1 = (pc % kL1Size) / kL2Size; size_t l2 = tab_->l1[reg][l1]; if (l2 == 0) { if (!add) return {const_cast(empty), 0}; l2 = ++alloc_; tab_->l1[reg][l1] = l2; if ((tab_->l2[l2 - 1] + 1) > reinterpret_cast(tab_) + kMemSize) Overflow(pc); } size_t off = (pc % kL2Size) / kCompression; size_t shift = (pc / kPCDivider) % kByteBits; return {tab_->l2[l2 - 1][off], 1 << shift}; } size_t FindRegion(uint64 pc, bool add = false) { const uint64 reg = pc | (kL1Size - 1); for (size_t r = 0; r < kNumRegions; r++) { if (tab_->regions[r] == reg) return r; } if (!add) return kNoRegion; for (size_t r = 0; r < kNumRegions; r++) { if (tab_->regions[r] == 0) { tab_->regions[r] = reg; return r; } } Overflow(pc); } NORETURN void Overflow(uint64 pc) { failmsg("coverage filter is full", "pc=0x%llx regions=[0x%llx 0x%llx 0x%llx 0x%llx] alloc=%u", pc, tab_->regions[0], tab_->regions[1], tab_->regions[2], tab_->regions[3], alloc_); } CoverFilter(const CoverFilter&) = delete; CoverFilter& operator=(const CoverFilter&) = delete; };