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 */