1 | /* |
2 | * This file is part of the KDE project. |
3 | * |
4 | * SPDX-License-Identifier: LGPL-2.0-only |
5 | */ |
6 | |
7 | #ifndef KSDCMAPPING_P_H |
8 | #define KSDCMAPPING_P_H |
9 | |
10 | #include "kcoreaddons_debug.h" |
11 | #include "ksdcmemory_p.h" |
12 | #include "kshareddatacache.h" |
13 | |
14 | #include <config-caching.h> // HAVE_SYS_MMAN_H |
15 | |
16 | #include <QFile> |
17 | #include <QtGlobal> |
18 | #include <qplatformdefs.h> |
19 | |
20 | #include <sys/resource.h> |
21 | |
22 | #if defined(_POSIX_MAPPED_FILES) && ((_POSIX_MAPPED_FILES == 0) || (_POSIX_MAPPED_FILES >= 200112L)) |
23 | #define KSDC_MAPPED_FILES_SUPPORTED 1 |
24 | #endif |
25 | |
26 | #if defined(_POSIX_SYNCHRONIZED_IO) && ((_POSIX_SYNCHRONIZED_IO == 0) || (_POSIX_SYNCHRONIZED_IO >= 200112L)) |
27 | #define KSDC_SYNCHRONIZED_IO_SUPPORTED 1 |
28 | #endif |
29 | |
30 | // msync(2) requires both MAPPED_FILES and SYNCHRONIZED_IO POSIX options |
31 | #if defined(KSDC_MAPPED_FILES_SUPPORTED) && defined(KSDC_SYNCHRONIZED_IO_SUPPORTED) |
32 | #define KSDC_MSYNC_SUPPORTED |
33 | #endif |
34 | |
35 | // BSD/Mac OS X compat |
36 | #if HAVE_SYS_MMAN_H |
37 | #include <sys/mman.h> |
38 | #endif |
39 | #if !defined(MAP_ANONYMOUS) && defined(MAP_ANON) |
40 | #define MAP_ANONYMOUS MAP_ANON |
41 | #endif |
42 | |
43 | class Q_DECL_HIDDEN KSDCMapping |
44 | { |
45 | public: |
46 | KSDCMapping(const QFile *file, const uint size, const uint cacheSize, const uint pageSize) |
47 | : m_mapped(nullptr) |
48 | , m_lock() |
49 | , m_mapSize(size) |
50 | , m_expectedType(LOCKTYPE_INVALID) |
51 | { |
52 | mapSharedMemory(file, size, cacheSize, pageSize); |
53 | } |
54 | |
55 | ~KSDCMapping() |
56 | { |
57 | detachFromSharedMemory(flush: true); |
58 | } |
59 | |
60 | bool isValid() |
61 | { |
62 | return !!m_mapped; |
63 | } |
64 | |
65 | bool lock() const |
66 | { |
67 | if (Q_UNLIKELY(!m_mapped)) { |
68 | return false; |
69 | } |
70 | if (Q_LIKELY(m_mapped->shmLock.type == m_expectedType)) { |
71 | return m_lock->lock(); |
72 | } |
73 | |
74 | // Wrong type --> corrupt! |
75 | throw KSDCCorrupted("Invalid cache lock type!" ); |
76 | } |
77 | |
78 | void unlock() const |
79 | { |
80 | if (Q_LIKELY(m_lock)) { |
81 | m_lock->unlock(); |
82 | } |
83 | } |
84 | |
85 | // This should be called for any memory access to shared memory. This |
86 | // function will verify that the bytes [base, base+accessLength) are |
87 | // actually mapped to m_mapped. The cache itself may have incorrect cache |
88 | // page sizes, incorrect cache size, etc. so this function should be called |
89 | // despite the cache data indicating it should be safe. |
90 | // |
91 | // If the access is /not/ safe then a KSDCCorrupted exception will be |
92 | // thrown, so be ready to catch that. |
93 | void verifyProposedMemoryAccess(const void *base, unsigned accessLength) const |
94 | { |
95 | quintptr startOfAccess = reinterpret_cast<quintptr>(base); |
96 | quintptr startOfShm = reinterpret_cast<quintptr>(m_mapped); |
97 | |
98 | if (Q_UNLIKELY(startOfAccess < startOfShm)) { |
99 | throw KSDCCorrupted(); |
100 | } |
101 | |
102 | quintptr endOfShm = startOfShm + m_mapSize; |
103 | quintptr endOfAccess = startOfAccess + accessLength; |
104 | |
105 | // Check for unsigned integer wraparound, and then |
106 | // bounds access |
107 | if (Q_UNLIKELY((endOfShm < startOfShm) || (endOfAccess < startOfAccess) || (endOfAccess > endOfShm))) { |
108 | throw KSDCCorrupted(); |
109 | } |
110 | } |
111 | |
112 | // Runs a quick battery of tests on an already-locked cache and returns |
113 | // false as soon as a sanity check fails. The cache remains locked in this |
114 | // situation. |
115 | bool isLockedCacheSafe() const |
116 | { |
117 | if (Q_UNLIKELY(!m_mapped)) { |
118 | return false; |
119 | } |
120 | |
121 | // Note that cachePageSize() itself runs a check that can throw. |
122 | uint testSize = SharedMemory::totalSize(cacheSize: m_mapped->cacheSize, effectivePageSize: m_mapped->cachePageSize()); |
123 | |
124 | if (Q_UNLIKELY(m_mapSize != testSize)) { |
125 | return false; |
126 | } |
127 | if (Q_UNLIKELY(m_mapped->version != SharedMemory::PIXMAP_CACHE_VERSION)) { |
128 | return false; |
129 | } |
130 | switch (m_mapped->evictionPolicy.loadRelaxed()) { |
131 | case KSharedDataCache::NoEvictionPreference: // fallthrough |
132 | case KSharedDataCache::EvictLeastRecentlyUsed: // fallthrough |
133 | case KSharedDataCache::EvictLeastOftenUsed: // fallthrough |
134 | case KSharedDataCache::EvictOldest: |
135 | break; |
136 | default: |
137 | return false; |
138 | } |
139 | |
140 | return true; |
141 | } |
142 | |
143 | SharedMemory *m_mapped; |
144 | |
145 | private: |
146 | // Put the cache in a condition to be able to call mapSharedMemory() by |
147 | // completely detaching from shared memory (such as to respond to an |
148 | // unrecoverable error). |
149 | // m_mapSize must already be set to the amount of memory mapped to m_mapped. |
150 | void detachFromSharedMemory(const bool flush = false) |
151 | { |
152 | // The lock holds a reference into shared memory, so this must be |
153 | // cleared before m_mapped is removed. |
154 | m_lock.reset(); |
155 | |
156 | // Note that there is no other actions required to separate from the |
157 | // shared memory segment, simply unmapping is enough. This makes things |
158 | // *much* easier so I'd recommend maintaining this ideal. |
159 | if (m_mapped) { |
160 | #ifdef KSDC_MSYNC_SUPPORTED |
161 | if (flush) { |
162 | ::msync(addr: m_mapped, len: m_mapSize, MS_INVALIDATE | MS_ASYNC); |
163 | } |
164 | #endif |
165 | ::munmap(addr: m_mapped, len: m_mapSize); |
166 | if (0 != ::munmap(addr: m_mapped, len: m_mapSize)) { |
167 | qCCritical(KCOREADDONS_DEBUG) << "Unable to unmap shared memory segment" << static_cast<void *>(m_mapped) << ":" << ::strerror(errno); |
168 | } |
169 | } |
170 | |
171 | // Do not delete m_mapped, it was never constructed, it's just an alias. |
172 | m_mapped = nullptr; |
173 | m_mapSize = 0; |
174 | } |
175 | |
176 | // This function does a lot of the important work, attempting to connect to shared |
177 | // memory, a private anonymous mapping if that fails, and failing that, nothing (but |
178 | // the cache remains "valid", we just don't actually do anything). |
179 | void mapSharedMemory(const QFile *file, uint size, uint cacheSize, uint pageSize) |
180 | { |
181 | void *mapAddress = MAP_FAILED; |
182 | |
183 | if (file) { |
184 | // Use mmap directly instead of QFile::map since the QFile (and its |
185 | // shared mapping) will disappear unless we hang onto the QFile for no |
186 | // reason (see the note below, we don't care about the file per se...) |
187 | mapAddress = QT_MMAP(addr: nullptr, len: size, PROT_READ | PROT_WRITE, MAP_SHARED, fd: file->handle(), offset: 0); |
188 | |
189 | // So... it is possible that someone else has mapped this cache already |
190 | // with a larger size. If that's the case we need to at least match |
191 | // the size to be able to access every entry, so fixup the mapping. |
192 | if (mapAddress != MAP_FAILED) { |
193 | // Successful mmap doesn't actually mean that whole range is readable so ensure it is |
194 | struct rlimit memlock; |
195 | if (getrlimit(RLIMIT_MEMLOCK, rlimits: &memlock) == 0 && memlock.rlim_cur >= 2) { |
196 | // Half of limit in case something else has already locked some mem |
197 | uint lockSize = qMin(a: memlock.rlim_cur / 2, b: (rlim_t)size); |
198 | // Note that lockSize might be less than what we need to mmap |
199 | // and so this doesn't guarantee that later parts will be readable |
200 | // but that's fine, at least we know we will succeed here |
201 | if (mlock(addr: mapAddress, len: lockSize)) { |
202 | throw KSDCCorrupted(QLatin1String("Cache is inaccessible " ) + file->fileName()); |
203 | } |
204 | if (munlock(addr: mapAddress, len: lockSize) != 0) { |
205 | qCDebug(KCOREADDONS_DEBUG) << "Failed to munlock!" ; |
206 | } |
207 | } else { |
208 | qCWarning(KCOREADDONS_DEBUG) << "Failed to get RLIMIT_MEMLOCK!" ; |
209 | } |
210 | |
211 | SharedMemory *mapped = reinterpret_cast<SharedMemory *>(mapAddress); |
212 | |
213 | // First make sure that the version of the cache on disk is |
214 | // valid. We also need to check that version != 0 to |
215 | // disambiguate against an uninitialized cache. |
216 | if (mapped->version != SharedMemory::PIXMAP_CACHE_VERSION && mapped->version > 0) { |
217 | detachFromSharedMemory(flush: false); |
218 | throw KSDCCorrupted(QLatin1String("Wrong version of cache " ) + file->fileName()); |
219 | } else if (mapped->cacheSize > cacheSize) { |
220 | // This order is very important. We must save the cache size |
221 | // before we remove the mapping, but unmap before overwriting |
222 | // the previous mapping size... |
223 | auto actualCacheSize = mapped->cacheSize; |
224 | auto actualPageSize = mapped->cachePageSize(); |
225 | ::munmap(addr: mapAddress, len: size); |
226 | size = SharedMemory::totalSize(cacheSize, effectivePageSize: pageSize); |
227 | mapAddress = QT_MMAP(addr: nullptr, len: size, PROT_READ | PROT_WRITE, MAP_SHARED, fd: file->handle(), offset: 0); |
228 | if (mapAddress != MAP_FAILED) { |
229 | cacheSize = actualCacheSize; |
230 | pageSize = actualPageSize; |
231 | } |
232 | } |
233 | } |
234 | } |
235 | |
236 | // We could be here without the mapping established if: |
237 | // 1) Process-shared synchronization is not supported, either at compile or run time, |
238 | // 2) Unable to open the required file. |
239 | // 3) Unable to resize the file to be large enough. |
240 | // 4) Establishing the mapping failed. |
241 | // 5) The mapping succeeded, but the size was wrong and we were unable to map when |
242 | // we tried again. |
243 | // 6) The incorrect version of the cache was detected. |
244 | // 7) The file could be created, but posix_fallocate failed to commit it fully to disk. |
245 | // In any of these cases, attempt to fallback to the |
246 | // better-supported anonymous page style of mmap. |
247 | // NOTE: We never use the on-disk representation independently of the |
248 | // shared memory. If we don't get shared memory the disk info is ignored, |
249 | // if we do get shared memory we never look at disk again. |
250 | if (!file || mapAddress == MAP_FAILED) { |
251 | qCWarning(KCOREADDONS_DEBUG) << "Couldn't establish file backed memory mapping, will fallback" |
252 | << "to anonymous memory" ; |
253 | mapAddress = QT_MMAP(addr: nullptr, len: size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, fd: -1, offset: 0); |
254 | } |
255 | |
256 | // Well now we're really hosed. We can still work, but we can't even cache |
257 | // data. |
258 | if (mapAddress == MAP_FAILED) { |
259 | qCCritical(KCOREADDONS_DEBUG) << "Unable to allocate shared memory segment for shared data cache" << file->fileName() << "of size" << m_mapSize; |
260 | m_mapped = nullptr; |
261 | m_mapSize = 0; |
262 | return; |
263 | } |
264 | |
265 | m_mapSize = size; |
266 | |
267 | // We never actually construct m_mapped, but we assign it the same address as the |
268 | // shared memory we just mapped, so effectively m_mapped is now a SharedMemory that |
269 | // happens to be located at mapAddress. |
270 | m_mapped = reinterpret_cast<SharedMemory *>(mapAddress); |
271 | |
272 | // If we were first to create this memory map, all data will be 0. |
273 | // Therefore if ready == 0 we're not initialized. A fully initialized |
274 | // header will have ready == 2. Why? |
275 | // Because 0 means "safe to initialize" |
276 | // 1 means "in progress of initing" |
277 | // 2 means "ready" |
278 | uint usecSleepTime = 8; // Start by sleeping for 8 microseconds |
279 | while (m_mapped->ready.loadRelaxed() != 2) { |
280 | if (Q_UNLIKELY(usecSleepTime >= (1 << 21))) { |
281 | // Didn't acquire within ~8 seconds? Assume an issue exists |
282 | detachFromSharedMemory(flush: false); |
283 | throw KSDCCorrupted("Unable to acquire shared lock, is the cache corrupt?" ); |
284 | } |
285 | |
286 | if (m_mapped->ready.testAndSetAcquire(expectedValue: 0, newValue: 1)) { |
287 | if (!m_mapped->performInitialSetup(cacheSize: cacheSize, pageSize: pageSize)) { |
288 | qCCritical(KCOREADDONS_DEBUG) << "Unable to perform initial setup, this system probably " |
289 | "does not really support process-shared pthreads or " |
290 | "semaphores, even though it claims otherwise." ; |
291 | |
292 | detachFromSharedMemory(flush: false); |
293 | return; |
294 | } |
295 | } else { |
296 | usleep(useconds: usecSleepTime); // spin |
297 | |
298 | // Exponential fallback as in Ethernet and similar collision resolution methods |
299 | usecSleepTime *= 2; |
300 | } |
301 | } |
302 | |
303 | m_expectedType = m_mapped->shmLock.type; |
304 | m_lock.reset(p: createLockFromId(id: m_expectedType, lock&: m_mapped->shmLock)); |
305 | bool isProcessSharingSupported = false; |
306 | |
307 | if (!m_lock->initialize(processSharingSupported&: isProcessSharingSupported)) { |
308 | qCCritical(KCOREADDONS_DEBUG) << "Unable to setup shared cache lock, although it worked when created." ; |
309 | detachFromSharedMemory(flush: false); |
310 | return; |
311 | } |
312 | } |
313 | |
314 | std::unique_ptr<KSDCLock> m_lock; |
315 | uint m_mapSize; |
316 | SharedLockId m_expectedType; |
317 | }; |
318 | |
319 | #endif /* KSDCMEMORY_P_H */ |
320 | |