298 lines
10 KiB
Diff
298 lines
10 KiB
Diff
--- a/src/lib/fcitx/icontheme.cpp
|
|
+++ b/src/lib/fcitx/icontheme.cpp
|
|
@@ -7,9 +7,11 @@
|
|
#include "icontheme.h"
|
|
#include <fcntl.h>
|
|
#include <sys/stat.h>
|
|
+#include <algorithm>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
+#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <functional>
|
|
@@ -39,10 +41,6 @@
|
|
#include "config.h" // IWYU pragma: keep
|
|
#include "misc_p.h"
|
|
|
|
-#ifdef HAVE_SYS_MMAN_H
|
|
-#include <sys/mman.h>
|
|
-#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<signed char>(*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<uint64_t>(st.st_size) > maxCacheSize) {
|
|
return;
|
|
}
|
|
|
|
- memory_ = static_cast<uint8_t *>(
|
|
- 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<size_t>(dirListOffset) + 4,
|
|
+ static_cast<size_t>(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<char *>(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<size_t>(1024, memory_.size() - offset);
|
|
+ auto *str = reinterpret_cast<const char *>(memory_.data() + offset);
|
|
+ if (!std::memchr(str, '\0', length)) {
|
|
+ isValid_ = false;
|
|
+ return nullptr;
|
|
+ }
|
|
+ return str;
|
|
}
|
|
|
|
std::unordered_set<std::string> lookup(const std::string &name) const;
|
|
|
|
mutable bool isValid_ = false;
|
|
- uint8_t *memory_ = nullptr;
|
|
- size_t size_ = 0;
|
|
+ std::vector<uint8_t> memory_;
|
|
};
|
|
|
|
std::unordered_set<std::string>
|
|
@@ -343,27 +344,43 @@
|
|
std::unordered_set<std::string> 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<size_t>(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<size_t> 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<size_t>(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<std::string> 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<std::string> dirFilter;
|
|
if (baseDir.second.isValid()) {
|
|
dirFilter = baseDir.second.lookup(iconname);
|
|
- hasCache = true;
|
|
+ hasCache = baseDir.second.isValid();
|
|
}
|
|
|
|
auto checkDirectoryWithSize =
|