File indexing completed on 2024-05-19 03:56:18
0001 /* 0002 * This file is part of the KDE project. 0003 * 0004 * SPDX-License-Identifier: LGPL-2.0-only 0005 */ 0006 0007 #ifndef KSDCMAPPING_P_H 0008 #define KSDCMAPPING_P_H 0009 0010 #include "kcoreaddons_debug.h" 0011 #include "ksdcmemory_p.h" 0012 #include "kshareddatacache.h" 0013 0014 #include <config-caching.h> // HAVE_SYS_MMAN_H 0015 0016 #include <QFile> 0017 #include <QtGlobal> 0018 #include <qplatformdefs.h> 0019 0020 #include <sys/resource.h> 0021 0022 #if defined(_POSIX_MAPPED_FILES) && ((_POSIX_MAPPED_FILES == 0) || (_POSIX_MAPPED_FILES >= 200112L)) 0023 #define KSDC_MAPPED_FILES_SUPPORTED 1 0024 #endif 0025 0026 #if defined(_POSIX_SYNCHRONIZED_IO) && ((_POSIX_SYNCHRONIZED_IO == 0) || (_POSIX_SYNCHRONIZED_IO >= 200112L)) 0027 #define KSDC_SYNCHRONIZED_IO_SUPPORTED 1 0028 #endif 0029 0030 // msync(2) requires both MAPPED_FILES and SYNCHRONIZED_IO POSIX options 0031 #if defined(KSDC_MAPPED_FILES_SUPPORTED) && defined(KSDC_SYNCHRONIZED_IO_SUPPORTED) 0032 #define KSDC_MSYNC_SUPPORTED 0033 #endif 0034 0035 // BSD/Mac OS X compat 0036 #if HAVE_SYS_MMAN_H 0037 #include <sys/mman.h> 0038 #endif 0039 #if !defined(MAP_ANONYMOUS) && defined(MAP_ANON) 0040 #define MAP_ANONYMOUS MAP_ANON 0041 #endif 0042 0043 class Q_DECL_HIDDEN KSDCMapping 0044 { 0045 public: 0046 KSDCMapping(const QFile *file, const uint size, const uint cacheSize, const uint pageSize) 0047 : m_mapped(nullptr) 0048 , m_lock() 0049 , m_mapSize(size) 0050 , m_expectedType(LOCKTYPE_INVALID) 0051 { 0052 mapSharedMemory(file, size, cacheSize, pageSize); 0053 } 0054 0055 ~KSDCMapping() 0056 { 0057 detachFromSharedMemory(true); 0058 } 0059 0060 bool isValid() 0061 { 0062 return !!m_mapped; 0063 } 0064 0065 bool lock() const 0066 { 0067 if (Q_UNLIKELY(!m_mapped)) { 0068 return false; 0069 } 0070 if (Q_LIKELY(m_mapped->shmLock.type == m_expectedType)) { 0071 return m_lock->lock(); 0072 } 0073 0074 // Wrong type --> corrupt! 0075 throw KSDCCorrupted("Invalid cache lock type!"); 0076 } 0077 0078 void unlock() const 0079 { 0080 if (Q_LIKELY(m_lock)) { 0081 m_lock->unlock(); 0082 } 0083 } 0084 0085 // This should be called for any memory access to shared memory. This 0086 // function will verify that the bytes [base, base+accessLength) are 0087 // actually mapped to m_mapped. The cache itself may have incorrect cache 0088 // page sizes, incorrect cache size, etc. so this function should be called 0089 // despite the cache data indicating it should be safe. 0090 // 0091 // If the access is /not/ safe then a KSDCCorrupted exception will be 0092 // thrown, so be ready to catch that. 0093 void verifyProposedMemoryAccess(const void *base, unsigned accessLength) const 0094 { 0095 quintptr startOfAccess = reinterpret_cast<quintptr>(base); 0096 quintptr startOfShm = reinterpret_cast<quintptr>(m_mapped); 0097 0098 if (Q_UNLIKELY(startOfAccess < startOfShm)) { 0099 throw KSDCCorrupted(); 0100 } 0101 0102 quintptr endOfShm = startOfShm + m_mapSize; 0103 quintptr endOfAccess = startOfAccess + accessLength; 0104 0105 // Check for unsigned integer wraparound, and then 0106 // bounds access 0107 if (Q_UNLIKELY((endOfShm < startOfShm) || (endOfAccess < startOfAccess) || (endOfAccess > endOfShm))) { 0108 throw KSDCCorrupted(); 0109 } 0110 } 0111 0112 // Runs a quick battery of tests on an already-locked cache and returns 0113 // false as soon as a sanity check fails. The cache remains locked in this 0114 // situation. 0115 bool isLockedCacheSafe() const 0116 { 0117 if (Q_UNLIKELY(!m_mapped)) { 0118 return false; 0119 } 0120 0121 // Note that cachePageSize() itself runs a check that can throw. 0122 uint testSize = SharedMemory::totalSize(m_mapped->cacheSize, m_mapped->cachePageSize()); 0123 0124 if (Q_UNLIKELY(m_mapSize != testSize)) { 0125 return false; 0126 } 0127 if (Q_UNLIKELY(m_mapped->version != SharedMemory::PIXMAP_CACHE_VERSION)) { 0128 return false; 0129 } 0130 switch (m_mapped->evictionPolicy.loadRelaxed()) { 0131 case KSharedDataCache::NoEvictionPreference: // fallthrough 0132 case KSharedDataCache::EvictLeastRecentlyUsed: // fallthrough 0133 case KSharedDataCache::EvictLeastOftenUsed: // fallthrough 0134 case KSharedDataCache::EvictOldest: 0135 break; 0136 default: 0137 return false; 0138 } 0139 0140 return true; 0141 } 0142 0143 SharedMemory *m_mapped; 0144 0145 private: 0146 // Put the cache in a condition to be able to call mapSharedMemory() by 0147 // completely detaching from shared memory (such as to respond to an 0148 // unrecoverable error). 0149 // m_mapSize must already be set to the amount of memory mapped to m_mapped. 0150 void detachFromSharedMemory(const bool flush = false) 0151 { 0152 // The lock holds a reference into shared memory, so this must be 0153 // cleared before m_mapped is removed. 0154 m_lock.reset(); 0155 0156 // Note that there is no other actions required to separate from the 0157 // shared memory segment, simply unmapping is enough. This makes things 0158 // *much* easier so I'd recommend maintaining this ideal. 0159 if (m_mapped) { 0160 #ifdef KSDC_MSYNC_SUPPORTED 0161 if (flush) { 0162 ::msync(m_mapped, m_mapSize, MS_INVALIDATE | MS_ASYNC); 0163 } 0164 #endif 0165 ::munmap(m_mapped, m_mapSize); 0166 if (0 != ::munmap(m_mapped, m_mapSize)) { 0167 qCCritical(KCOREADDONS_DEBUG) << "Unable to unmap shared memory segment" << static_cast<void *>(m_mapped) << ":" << ::strerror(errno); 0168 } 0169 } 0170 0171 // Do not delete m_mapped, it was never constructed, it's just an alias. 0172 m_mapped = nullptr; 0173 m_mapSize = 0; 0174 } 0175 0176 // This function does a lot of the important work, attempting to connect to shared 0177 // memory, a private anonymous mapping if that fails, and failing that, nothing (but 0178 // the cache remains "valid", we just don't actually do anything). 0179 void mapSharedMemory(const QFile *file, uint size, uint cacheSize, uint pageSize) 0180 { 0181 void *mapAddress = MAP_FAILED; 0182 0183 if (file) { 0184 // Use mmap directly instead of QFile::map since the QFile (and its 0185 // shared mapping) will disappear unless we hang onto the QFile for no 0186 // reason (see the note below, we don't care about the file per se...) 0187 mapAddress = QT_MMAP(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, file->handle(), 0); 0188 0189 // So... it is possible that someone else has mapped this cache already 0190 // with a larger size. If that's the case we need to at least match 0191 // the size to be able to access every entry, so fixup the mapping. 0192 if (mapAddress != MAP_FAILED) { 0193 // Successful mmap doesn't actually mean that whole range is readable so ensure it is 0194 struct rlimit memlock; 0195 if (getrlimit(RLIMIT_MEMLOCK, &memlock) == 0 && memlock.rlim_cur >= 2) { 0196 // Half of limit in case something else has already locked some mem 0197 uint lockSize = qMin(memlock.rlim_cur / 2, (rlim_t)size); 0198 // Note that lockSize might be less than what we need to mmap 0199 // and so this doesn't guarantee that later parts will be readable 0200 // but that's fine, at least we know we will succeed here 0201 if (mlock(mapAddress, lockSize)) { 0202 throw KSDCCorrupted(QLatin1String("Cache is inaccessible ") + file->fileName()); 0203 } 0204 if (munlock(mapAddress, lockSize) != 0) { 0205 qCDebug(KCOREADDONS_DEBUG) << "Failed to munlock!"; 0206 } 0207 } else { 0208 qCWarning(KCOREADDONS_DEBUG) << "Failed to get RLIMIT_MEMLOCK!"; 0209 } 0210 0211 SharedMemory *mapped = reinterpret_cast<SharedMemory *>(mapAddress); 0212 0213 // First make sure that the version of the cache on disk is 0214 // valid. We also need to check that version != 0 to 0215 // disambiguate against an uninitialized cache. 0216 if (mapped->version != SharedMemory::PIXMAP_CACHE_VERSION && mapped->version > 0) { 0217 detachFromSharedMemory(false); 0218 throw KSDCCorrupted(QLatin1String("Wrong version of cache ") + file->fileName()); 0219 } else if (mapped->cacheSize > cacheSize) { 0220 // This order is very important. We must save the cache size 0221 // before we remove the mapping, but unmap before overwriting 0222 // the previous mapping size... 0223 auto actualCacheSize = mapped->cacheSize; 0224 auto actualPageSize = mapped->cachePageSize(); 0225 ::munmap(mapAddress, size); 0226 size = SharedMemory::totalSize(cacheSize, pageSize); 0227 mapAddress = QT_MMAP(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, file->handle(), 0); 0228 if (mapAddress != MAP_FAILED) { 0229 cacheSize = actualCacheSize; 0230 pageSize = actualPageSize; 0231 } 0232 } 0233 } 0234 } 0235 0236 // We could be here without the mapping established if: 0237 // 1) Process-shared synchronization is not supported, either at compile or run time, 0238 // 2) Unable to open the required file. 0239 // 3) Unable to resize the file to be large enough. 0240 // 4) Establishing the mapping failed. 0241 // 5) The mapping succeeded, but the size was wrong and we were unable to map when 0242 // we tried again. 0243 // 6) The incorrect version of the cache was detected. 0244 // 7) The file could be created, but posix_fallocate failed to commit it fully to disk. 0245 // In any of these cases, attempt to fallback to the 0246 // better-supported anonymous page style of mmap. 0247 // NOTE: We never use the on-disk representation independently of the 0248 // shared memory. If we don't get shared memory the disk info is ignored, 0249 // if we do get shared memory we never look at disk again. 0250 if (!file || mapAddress == MAP_FAILED) { 0251 qCWarning(KCOREADDONS_DEBUG) << "Couldn't establish file backed memory mapping, will fallback" 0252 << "to anonymous memory"; 0253 mapAddress = QT_MMAP(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); 0254 } 0255 0256 // Well now we're really hosed. We can still work, but we can't even cache 0257 // data. 0258 if (mapAddress == MAP_FAILED) { 0259 qCCritical(KCOREADDONS_DEBUG) << "Unable to allocate shared memory segment for shared data cache" << file->fileName() << "of size" << m_mapSize; 0260 m_mapped = nullptr; 0261 m_mapSize = 0; 0262 return; 0263 } 0264 0265 m_mapSize = size; 0266 0267 // We never actually construct m_mapped, but we assign it the same address as the 0268 // shared memory we just mapped, so effectively m_mapped is now a SharedMemory that 0269 // happens to be located at mapAddress. 0270 m_mapped = reinterpret_cast<SharedMemory *>(mapAddress); 0271 0272 // If we were first to create this memory map, all data will be 0. 0273 // Therefore if ready == 0 we're not initialized. A fully initialized 0274 // header will have ready == 2. Why? 0275 // Because 0 means "safe to initialize" 0276 // 1 means "in progress of initing" 0277 // 2 means "ready" 0278 uint usecSleepTime = 8; // Start by sleeping for 8 microseconds 0279 while (m_mapped->ready.loadRelaxed() != 2) { 0280 if (Q_UNLIKELY(usecSleepTime >= (1 << 21))) { 0281 // Didn't acquire within ~8 seconds? Assume an issue exists 0282 detachFromSharedMemory(false); 0283 throw KSDCCorrupted("Unable to acquire shared lock, is the cache corrupt?"); 0284 } 0285 0286 if (m_mapped->ready.testAndSetAcquire(0, 1)) { 0287 if (!m_mapped->performInitialSetup(cacheSize, pageSize)) { 0288 qCCritical(KCOREADDONS_DEBUG) << "Unable to perform initial setup, this system probably " 0289 "does not really support process-shared pthreads or " 0290 "semaphores, even though it claims otherwise."; 0291 0292 detachFromSharedMemory(false); 0293 return; 0294 } 0295 } else { 0296 usleep(usecSleepTime); // spin 0297 0298 // Exponential fallback as in Ethernet and similar collision resolution methods 0299 usecSleepTime *= 2; 0300 } 0301 } 0302 0303 m_expectedType = m_mapped->shmLock.type; 0304 m_lock.reset(createLockFromId(m_expectedType, m_mapped->shmLock)); 0305 bool isProcessSharingSupported = false; 0306 0307 if (!m_lock->initialize(isProcessSharingSupported)) { 0308 qCCritical(KCOREADDONS_DEBUG) << "Unable to setup shared cache lock, although it worked when created."; 0309 detachFromSharedMemory(false); 0310 return; 0311 } 0312 } 0313 0314 std::unique_ptr<KSDCLock> m_lock; 0315 uint m_mapSize; 0316 SharedLockId m_expectedType; 0317 }; 0318 0319 #endif /* KSDCMEMORY_P_H */