--- a/src/lib/fcitx/icontheme.cpp +++ b/src/lib/fcitx/icontheme.cpp @@ -7,9 +7,11 @@ #include "icontheme.h" #include #include +#include #include #include #include +#include #include #include #include @@ -39,10 +41,6 @@ #include "config.h" // IWYU pragma: keep #include "misc_p.h" -#ifdef HAVE_SYS_MMAN_H -#include -#endif - namespace { // helper type for the visitor @@ -194,6 +192,9 @@ FCITX_DEFINE_READ_ONLY_PROPERTY_PRIVATE(IconThemeDirectory, int, threshold); static uint32_t iconNameHash(const char *p) { + if (!*p) { + return 0; + } uint32_t h = static_cast(*p); for (p += 1; *p != '\0'; p++) { h = (h << 5) - h + *p; @@ -207,7 +208,8 @@ #ifdef _WIN32 FCITX_UNUSED(filename); #else - auto fd = StandardPaths::openPath(filename); + // Never block opening a FIFO masquerading as a cache. + auto fd = StandardPaths::openPath(filename, O_RDONLY | O_NONBLOCK); if (!fd.isValid()) { return; } @@ -225,35 +227,46 @@ } struct stat st; - if (fstat(fd.fd(), &st) < 0) { + constexpr size_t maxCacheSize = 16 * 1024 * 1024; + if (fstat(fd.fd(), &st) < 0 || !S_ISREG(st.st_mode) || + st.st_size < 12 || + static_cast(st.st_size) > maxCacheSize) { return; } - memory_ = static_cast( - mmap(nullptr, st.st_size, PROT_READ, MAP_SHARED, fd.fd(), 0)); - if (memory_ == MAP_FAILED) { - // Failed to mmap is not critical here. + // Own a bounded snapshot: a concurrent in-place truncation must not + // turn a cache lookup into SIGBUS. Invalid snapshots are discarded. + memory_.resize(st.st_size); + size_t done = 0; + while (done < memory_.size()) { + auto n = fs::safeRead(fd.fd(), memory_.data() + done, + memory_.size() - done); + if (n <= 0) { + memory_.clear(); + return; + } + done += n; + } + + isValid_ = true; + if (readWord(0) != 1 || readWord(2) != 0) { // version major minor + isValid_ = false; return; } - size_ = st.st_size; - - if (readWord(0) != 1 || readWord(2) != 0) { // version major minor - return; - } - - isValid_ = true; // Check that all the directories are older than the cache uint32_t dirListOffset = readDoubleWord(8); uint32_t dirListLen = readDoubleWord(dirListOffset); // We assume it won't be so long just in case we hit a corruptted file. - if (dirListLen > 4096) { + if (!isValid_ || dirListLen > 4096 || + !checkRange(static_cast(dirListOffset) + 4, + static_cast(dirListLen) * 4)) { isValid_ = false; return; } for (uint32_t i = 0; i < dirListLen; ++i) { uint32_t offset = readDoubleWord(dirListOffset + 4 + (4 * i)); - if (!isValid_ || offset >= size_) { + if (!isValid_) { isValid_ = false; return; } @@ -263,7 +276,8 @@ return; } std::filesystem::path subdirName(dir); - if (subdirName.is_absolute()) { + if (subdirName.is_absolute() || + std::ranges::find(subdirName, "..") != subdirName.end()) { isValid_ = false; return; } @@ -277,65 +291,52 @@ } IconThemeCache() = default; - IconThemeCache(IconThemeCache &&other) noexcept - : isValid_(other.isValid_), memory_(other.memory_), size_(other.size_) { - other.isValid_ = false; - other.memory_ = nullptr; - other.size_ = 0; - } - - IconThemeCache &operator=(IconThemeCache other) { - std::swap(other.isValid_, isValid_); - std::swap(other.size_, size_); - std::swap(other.memory_, memory_); - return *this; - } + IconThemeCache(IconThemeCache &&other) noexcept = default; + IconThemeCache &operator=(IconThemeCache &&other) noexcept = default; bool isValid() const { return isValid_; } - ~IconThemeCache() { -#ifndef _WIN32 - if (memory_) { - munmap(memory_, size_); - } -#endif - } - - uint16_t readWord(uint32_t offset) const { - if (offset + 2 > size_ || (offset % 2)) { + bool checkRange(size_t offset, size_t length) const { + if (offset > memory_.size() || length > memory_.size() - offset) { + isValid_ = false; + return false; + } + return true; + } + + uint16_t readWord(size_t offset) const { + if (!checkRange(offset, 2) || (offset % 2)) { isValid_ = false; return 0; } return memory_[offset + 1] | memory_[offset] << 8; } - uint32_t readDoubleWord(unsigned int offset) const { - if (offset + 4 > size_ || (offset % 4)) { + uint32_t readDoubleWord(size_t offset) const { + if (!checkRange(offset, 4) || (offset % 4)) { isValid_ = false; return 0; } - return memory_[offset + 3] | memory_[offset + 2] << 8 | - memory_[offset + 1] << 16 | memory_[offset] << 24; - } - - char *checkString(uint32_t offset) const { - // assume string length is less than 1k. - for (uint32_t i = 0; i < 1024; i++) { - if (offset + i >= size_) { - return nullptr; - } - auto c = memory_[offset + i]; - if (c == '\0') { - break; - } - } - return reinterpret_cast(memory_ + offset); + return uint32_t(memory_[offset + 3]) | uint32_t(memory_[offset + 2]) << 8 | + uint32_t(memory_[offset + 1]) << 16 | uint32_t(memory_[offset]) << 24; + } + + const char *checkString(size_t offset) const { + if (!checkRange(offset, 1)) { + return nullptr; + } + const auto length = std::min(1024, memory_.size() - offset); + auto *str = reinterpret_cast(memory_.data() + offset); + if (!std::memchr(str, '\0', length)) { + isValid_ = false; + return nullptr; + } + return str; } std::unordered_set lookup(const std::string &name) const; mutable bool isValid_ = false; - uint8_t *memory_ = nullptr; - size_t size_ = 0; + std::vector memory_; }; std::unordered_set @@ -343,27 +344,43 @@ std::unordered_set ret; auto hash = iconNameHash(name.c_str()); - uint32_t hashOffset = readDoubleWord(4); + if (!isValid_) { + return ret; + } + size_t hashOffset = readDoubleWord(4); uint32_t hashBucketCount = readDoubleWord(hashOffset); - if (!isValid_ || hashBucketCount == 0) { + if (!isValid_ || hashBucketCount == 0 || + hashBucketCount > memory_.size() / 4 || + !checkRange(hashOffset + 4, static_cast(hashBucketCount) * 4)) { isValid_ = false; return ret; } - uint32_t bucketIndex = hash % hashBucketCount; - uint32_t bucketOffset = readDoubleWord(hashOffset + 4 + (bucketIndex * 4)); - while (bucketOffset > 0 && bucketOffset <= size_ - 12) { + size_t bucketIndex = hash % hashBucketCount; + size_t bucketOffset = readDoubleWord(hashOffset + 4 + (bucketIndex * 4)); + std::unordered_set visited; + while (bucketOffset != 0 && bucketOffset != UINT32_MAX) { + // A cache is only an accelerator. Bound even acyclic hostile chains. + if (!isValid_ || !checkRange(bucketOffset, 12) || + visited.size() >= 4096 || !visited.insert(bucketOffset).second) { + isValid_ = false; + return {}; + } uint32_t nameOff = readDoubleWord(bucketOffset + 4); auto *namePtr = checkString(nameOff); - if (nameOff < size_ && namePtr && name == namePtr) { - uint32_t dirListOffset = readDoubleWord(8); + if (!isValid_ || !namePtr) { + return {}; + } + if (name == namePtr) { + size_t dirListOffset = readDoubleWord(8); uint32_t dirListLen = readDoubleWord(dirListOffset); - uint32_t listOffset = readDoubleWord(bucketOffset + 8); + size_t listOffset = readDoubleWord(bucketOffset + 8); uint32_t listLen = readDoubleWord(listOffset); - if (!isValid_ || listOffset + 4 + 8 * listLen > size_) { + if (!isValid_ || listLen > 4096 || + !checkRange(listOffset + 4, static_cast(listLen) * 8)) { isValid_ = false; return ret; } @@ -371,11 +388,11 @@ ret.reserve(listLen); for (uint32_t j = 0; j < listLen && isValid_; ++j) { uint32_t dirIndex = readWord(listOffset + 4 + (8 * j)); - uint32_t o = readDoubleWord(dirListOffset + 4 + (dirIndex * 4)); - if (!isValid_ || dirIndex >= dirListLen || o >= size_) { + if (!isValid_ || dirIndex >= dirListLen) { isValid_ = false; return ret; } + uint32_t o = readDoubleWord(dirListOffset + 4 + (dirIndex * 4)); if (auto *str = checkString(o)) { ret.emplace(str); } else { @@ -565,7 +582,7 @@ std::unordered_set dirFilter; if (baseDir.second.isValid()) { dirFilter = baseDir.second.lookup(iconname); - hasCache = true; + hasCache = baseDir.second.isValid(); } for (const auto &directory : directories_) { if ((hasCache && !dirFilter.contains(directory.path())) || @@ -600,7 +617,7 @@ std::unordered_set dirFilter; if (baseDir.second.isValid()) { dirFilter = baseDir.second.lookup(iconname); - hasCache = true; + hasCache = baseDir.second.isValid(); } auto checkDirectoryWithSize =