Line data Source code
1 : /******************************************************************************
2 : *
3 : * Name: cpl_userfaultfd.cpp
4 : * Project: CPL - Common Portability Library
5 : * Purpose: Use userfaultfd and VSIL to service page faults
6 : * Author: James McClain, <james.mcclain@gmail.com>
7 : *
8 : ******************************************************************************
9 : * Copyright (c) 2018, Dr. James McClain <james.mcclain@gmail.com>
10 : *
11 : * SPDX-License-Identifier: MIT
12 : ****************************************************************************/
13 :
14 : #ifdef ENABLE_UFFD
15 :
16 : #include <algorithm>
17 : #include <cstdlib>
18 : #include <cinttypes>
19 : #include <cstring>
20 : #include <string>
21 :
22 : #include <errno.h>
23 : #include <fcntl.h>
24 : #include <poll.h>
25 : #include <pthread.h>
26 : #include <sched.h>
27 : #include <signal.h>
28 : #include <unistd.h>
29 :
30 : #include <sys/ioctl.h>
31 : #include <sys/mman.h>
32 : #include <sys/stat.h>
33 : #include <sys/syscall.h>
34 : #include <sys/types.h>
35 : #include <sys/utsname.h>
36 : #include <linux/userfaultfd.h>
37 :
38 : #include "cpl_conv.h"
39 : #include "cpl_error.h"
40 : #include "cpl_userfaultfd.h"
41 : #include "cpl_string.h"
42 : #include "cpl_vsi.h"
43 : #include "cpl_multiproc.h"
44 :
45 : #ifndef UFFD_USER_MODE_ONLY
46 : // The UFFD_USER_MODE_ONLY flag got added in kernel 5.11 which is the one
47 : // used by Ubuntu 20.04, but the linux-libc-dev package corresponds to 5.4
48 : #define UFFD_USER_MODE_ONLY 1
49 : #endif
50 :
51 : #define BAD_MMAP (reinterpret_cast<void *>(-1))
52 : #define MAX_MESSAGES (0x100)
53 :
54 : static int64_t get_page_limit();
55 : static void cpl_uffd_fault_handler(void *ptr);
56 : static void signal_handler(int signal);
57 : static void uffd_cleanup(void *ptr);
58 :
59 : struct cpl_uffd_context
60 : {
61 : bool keep_going = false;
62 :
63 : int uffd = -1;
64 : struct uffdio_register uffdio_register = {};
65 : struct uffd_msg uffd_msgs[MAX_MESSAGES];
66 :
67 : std::string filename = std::string("");
68 :
69 : int64_t page_limit = -1;
70 : int64_t pages_used = 0;
71 :
72 : size_t file_size = 0;
73 : size_t page_size = 0;
74 : void *page_ptr = nullptr;
75 : size_t vma_size = 0;
76 : void *vma_ptr = nullptr;
77 : CPLJoinableThread *thread = nullptr;
78 : };
79 :
80 3 : static void uffd_cleanup(void *ptr)
81 : {
82 3 : struct cpl_uffd_context *ctx = static_cast<struct cpl_uffd_context *>(ptr);
83 :
84 3 : if (!ctx)
85 0 : return;
86 :
87 : // Signal shutdown
88 3 : ctx->keep_going = false;
89 3 : if (ctx->thread)
90 : {
91 3 : CPLJoinThread(ctx->thread);
92 3 : ctx->thread = nullptr;
93 : }
94 :
95 3 : if (ctx->uffd != -1)
96 : {
97 3 : ioctl(ctx->uffd, UFFDIO_UNREGISTER, &ctx->uffdio_register);
98 3 : close(ctx->uffd);
99 3 : ctx->uffd = -1;
100 : }
101 3 : if (ctx->page_ptr && ctx->page_size)
102 3 : munmap(ctx->page_ptr, ctx->page_size);
103 3 : if (ctx->vma_ptr && ctx->vma_size)
104 3 : munmap(ctx->vma_ptr, ctx->vma_size);
105 3 : ctx->page_ptr = nullptr;
106 3 : ctx->vma_ptr = nullptr;
107 3 : ctx->page_size = 0;
108 3 : ctx->vma_size = 0;
109 3 : ctx->pages_used = 0;
110 3 : ctx->page_limit = 0;
111 :
112 3 : delete ctx;
113 :
114 3 : return;
115 : }
116 :
117 : #ifdef HAVE_GCC_WARNING_ZERO_AS_NULL_POINTER_CONSTANT
118 : #pragma GCC diagnostic push
119 : #pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant"
120 : #endif
121 : static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
122 : #ifdef HAVE_GCC_WARNING_ZERO_AS_NULL_POINTER_CONSTANT
123 : #pragma GCC diagnostic pop
124 : #endif
125 :
126 3 : static int64_t get_page_limit()
127 : {
128 : int64_t retval;
129 3 : const char *variable = CPLGetConfigOption(GDAL_UFFD_LIMIT, nullptr);
130 :
131 3 : if (variable && sscanf(variable, "%" PRId64, &retval) == 1)
132 0 : return retval;
133 : else
134 3 : return -1;
135 : }
136 :
137 3 : static void cpl_uffd_fault_handler(void *ptr)
138 : {
139 3 : struct cpl_uffd_context *ctx = static_cast<struct cpl_uffd_context *>(ptr);
140 : struct uffdio_copy uffdio_copy;
141 : struct pollfd pollfd;
142 :
143 : // Setup pollfd structure
144 3 : pollfd.fd = ctx->uffd;
145 3 : pollfd.events = POLLIN;
146 :
147 : // Open asset for reading
148 3 : VSILFILE *file = VSIFOpenL(ctx->filename.c_str(), "rb");
149 :
150 3 : if (!file)
151 0 : return;
152 :
153 : // Loop until told to stop
154 24 : while (ctx->keep_going)
155 : {
156 : // Poll for event
157 21 : if (poll(&pollfd, 1, 16) == -1)
158 0 : break; // 60Hz when no demand
159 21 : if ((pollfd.revents & POLLERR) || (pollfd.revents & POLLNVAL))
160 : break;
161 21 : if (!(pollfd.revents & POLLIN))
162 18 : continue;
163 :
164 : // Read page fault events
165 : ssize_t bytes_read = static_cast<ssize_t>(
166 3 : read(ctx->uffd, ctx->uffd_msgs, MAX_MESSAGES * sizeof(uffd_msg)));
167 3 : if (bytes_read < 1)
168 : {
169 0 : if (errno == EWOULDBLOCK)
170 0 : continue;
171 : else
172 0 : break;
173 : }
174 :
175 : // If too many pages are in use, evict all pages (evict them from
176 : // RAM and swap, not just to swap). It is impossible to control
177 : // which/when threads access the VMA, so access to the VMA has to
178 : // forbidden while the activity is in progress.
179 : //
180 : // That is done by (1) installing special handlers for SIGSEGV and
181 : // SIGBUS, (2) mprotecting the VMA so that any threads accessing
182 : // it receive either SIGSEGV or SIGBUS (which one is apparently a
183 : // function of the C library, at least on one non-Linux GNU
184 : // system[1]), (3) unregistering the VMA from userfaultfd,
185 : // remapping the VMA to evict the pages, registering the VMA
186 : // again, (4) making the VMA accessible again, and finally (5)
187 : // restoring the previous signal-handling behavior.
188 : //
189 : // [1] https://lists.debian.org/debian-bsd/2011/05/msg00032.html
190 3 : if (ctx->page_limit > 0)
191 : {
192 0 : pthread_mutex_lock(&mutex);
193 0 : if (ctx->pages_used > ctx->page_limit)
194 : {
195 : struct sigaction segv;
196 : struct sigaction old_segv;
197 : struct sigaction bus;
198 : struct sigaction old_bus;
199 :
200 0 : memset(&segv, 0, sizeof(segv));
201 0 : memset(&old_segv, 0, sizeof(old_segv));
202 0 : memset(&bus, 0, sizeof(bus));
203 0 : memset(&old_bus, 0, sizeof(old_bus));
204 :
205 : // Step 1 from the block comment above
206 0 : segv.sa_handler = signal_handler;
207 0 : bus.sa_handler = signal_handler;
208 0 : if (sigaction(SIGSEGV, &segv, &old_segv) == -1)
209 : {
210 0 : CPLError(
211 : CE_Failure, CPLE_AppDefined,
212 : "cpl_uffd_fault_handler: sigaction(SIGSEGV) failed");
213 0 : pthread_mutex_unlock(&mutex);
214 0 : break;
215 : }
216 0 : if (sigaction(SIGBUS, &bus, &old_bus) == -1)
217 : {
218 0 : CPLError(
219 : CE_Failure, CPLE_AppDefined,
220 : "cpl_uffd_fault_handler: sigaction(SIGBUS) failed");
221 0 : pthread_mutex_unlock(&mutex);
222 0 : break;
223 : }
224 :
225 : // WARNING: LACK OF THREAD-SAFETY.
226 : //
227 : // For example, if a user program (or another part of the
228 : // library) installs a SIGSEGV or SIGBUS handler from another
229 : // thread after this one has installed its handlers but before
230 : // this one uninstalls its handlers, the intervening handler
231 : // will be eliminated. There are other examples, as well, but
232 : // there can only be a problems with other threads because the
233 : // faulting thread is blocked here.
234 : //
235 : // This implies that one should not use cpl_virtualmem.h API
236 : // while other threads are actively generating faults that use
237 : // this mechanism.
238 : //
239 : // Having multiple active threads that use this mechanism but
240 : // with no changes to signal-handling in other threads is NOT a
241 : // problem.
242 :
243 : // Step 2
244 0 : if (mprotect(ctx->vma_ptr, ctx->vma_size, PROT_NONE) == -1)
245 : {
246 0 : CPLError(CE_Failure, CPLE_AppDefined,
247 : "cpl_uffd_fault_handler: mprotect() failed");
248 0 : pthread_mutex_unlock(&mutex);
249 0 : break;
250 : }
251 :
252 : // Step 3
253 0 : if (ioctl(ctx->uffd, UFFDIO_UNREGISTER, &ctx->uffdio_register))
254 : {
255 0 : CPLError(CE_Failure, CPLE_AppDefined,
256 : "cpl_uffd_fault_handler: ioctl(UFFDIO_UNREGISTER) "
257 : "failed");
258 0 : pthread_mutex_unlock(&mutex);
259 0 : break;
260 : }
261 0 : ctx->vma_ptr =
262 0 : mmap(ctx->vma_ptr, ctx->vma_size, PROT_NONE,
263 : MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
264 0 : if (ctx->vma_ptr == BAD_MMAP)
265 : {
266 0 : CPLError(CE_Failure, CPLE_AppDefined,
267 : "cpl_uffd_fault_handler: mmap() failed");
268 0 : ctx->vma_ptr = nullptr;
269 0 : pthread_mutex_unlock(&mutex);
270 0 : break;
271 : }
272 0 : ctx->pages_used = 0;
273 0 : if (ioctl(ctx->uffd, UFFDIO_REGISTER, &ctx->uffdio_register))
274 : {
275 0 : CPLError(CE_Failure, CPLE_AppDefined,
276 : "cpl_uffd_fault_handler: ioctl(UFFDIO_REGISTER) "
277 : "failed");
278 0 : pthread_mutex_unlock(&mutex);
279 0 : break;
280 : }
281 :
282 : // Step 4. Problem: A thread might attempt to read here (before
283 : // the mprotect) and receive a SIGSEGV or SIGBUS.
284 0 : if (mprotect(ctx->vma_ptr, ctx->vma_size, PROT_READ) == -1)
285 : {
286 0 : CPLError(CE_Failure, CPLE_AppDefined,
287 : "cpl_uffd_fault_handler: mprotect() failed");
288 0 : pthread_mutex_unlock(&mutex);
289 0 : break;
290 : }
291 :
292 : // Step 5. Solution: Cannot unregister special handlers before
293 : // any such threads have been handled by them, so sleep for
294 : // 1/100th of a second.
295 : // Coverity complains about sleeping under a mutex
296 : #ifndef __COVERITY__
297 : // coverity[sleep]
298 0 : usleep(10000);
299 : #endif
300 0 : if (sigaction(SIGSEGV, &old_segv, nullptr) == -1)
301 : {
302 0 : CPLError(
303 : CE_Failure, CPLE_AppDefined,
304 : "cpl_uffd_fault_handler: sigaction(SIGSEGV) failed");
305 0 : pthread_mutex_unlock(&mutex);
306 0 : break;
307 : }
308 0 : if (sigaction(SIGBUS, &old_bus, nullptr) == -1)
309 : {
310 0 : CPLError(
311 : CE_Failure, CPLE_AppDefined,
312 : "cpl_uffd_fault_handler: sigaction(SIGBUS) failed");
313 0 : pthread_mutex_unlock(&mutex);
314 0 : break;
315 : }
316 : }
317 0 : pthread_mutex_unlock(&mutex);
318 : }
319 :
320 : // Handle page fault events
321 6 : for (int i = 0; i < static_cast<int>(bytes_read / sizeof(uffd_msg));
322 : ++i)
323 : {
324 3 : const uintptr_t fault_addr =
325 3 : ctx->uffd_msgs[i].arg.pagefault.address & ~(ctx->page_size - 1);
326 3 : const uintptr_t offset =
327 3 : fault_addr - reinterpret_cast<uintptr_t>(ctx->vma_ptr);
328 3 : size_t bytes_needed = static_cast<size_t>(ctx->file_size - offset);
329 3 : if (bytes_needed > ctx->page_size)
330 0 : bytes_needed = ctx->page_size;
331 :
332 : // Copy data into page
333 6 : if (VSIFSeekL(file, offset, SEEK_SET) != 0 ||
334 3 : VSIFReadL(ctx->page_ptr, bytes_needed, 1, file) != 1)
335 : {
336 0 : CPLError(CE_Failure, CPLE_FileIO,
337 : "Cannot get %d bytes at offset " CPL_FRMT_GUIB " of "
338 : "file %s",
339 : static_cast<int>(bytes_needed),
340 : static_cast<GUIntBig>(offset), ctx->filename.c_str());
341 0 : memset(ctx->page_ptr, 0, bytes_needed);
342 : }
343 3 : ctx->pages_used++;
344 :
345 : // Use the page to fulfill the page fault
346 3 : uffdio_copy.src = reinterpret_cast<uintptr_t>(ctx->page_ptr);
347 3 : uffdio_copy.dst = fault_addr;
348 3 : uffdio_copy.len = static_cast<uintptr_t>(ctx->page_size);
349 3 : uffdio_copy.mode = 0;
350 3 : uffdio_copy.copy = 0;
351 3 : if (ioctl(ctx->uffd, UFFDIO_COPY, &uffdio_copy) == -1)
352 : {
353 0 : CPLError(CE_Failure, CPLE_AppDefined,
354 : "ioctl(UFFDIO_COPY) failed");
355 0 : break;
356 : }
357 : }
358 : } // end of while loop
359 :
360 : // Return resources
361 3 : VSIFCloseL(file);
362 : }
363 :
364 0 : static void signal_handler(int signal)
365 : {
366 0 : if (signal == SIGSEGV || signal == SIGBUS)
367 0 : sched_yield();
368 0 : return;
369 : }
370 :
371 18 : bool CPLIsUserFaultMappingSupported()
372 : {
373 : // Check the Linux kernel version. Linux 4.3 or newer is needed for
374 : // userfaultfd.
375 18 : int major = 0, minor = 0;
376 : struct utsname utsname;
377 :
378 18 : if (uname(&utsname))
379 0 : return false;
380 18 : sscanf(utsname.release, "%d.%d", &major, &minor);
381 18 : if (major < 4)
382 0 : return false;
383 18 : if (major == 4 && minor < 3)
384 0 : return false;
385 :
386 : static int nEnableUserFaultFD = -1;
387 18 : if (nEnableUserFaultFD < 0)
388 : {
389 12 : nEnableUserFaultFD =
390 12 : CPLTestBool(CPLGetConfigOption("CPL_ENABLE_USERFAULTFD", "YES"));
391 : }
392 18 : if (!nEnableUserFaultFD)
393 0 : return false;
394 :
395 : // Since kernel 5.2, raw userfaultfd is disabled since if the fault
396 : // originates from the kernel, that could lead to easier exploitation of
397 : // kernel bugs. Since kernel 5.11, UFFD_USER_MODE_ONLY can be used to
398 : // restrict the mechanism to faults occurring only from user space, which is
399 : // likely to be our use case.
400 18 : int uffd = static_cast<int>(syscall(
401 18 : __NR_userfaultfd, O_CLOEXEC | O_NONBLOCK | UFFD_USER_MODE_ONLY));
402 18 : if (uffd == -1 && errno == EINVAL)
403 0 : uffd =
404 0 : static_cast<int>(syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK));
405 18 : if (uffd == -1)
406 : {
407 0 : const int l_errno = errno;
408 0 : if (l_errno == EPERM)
409 : {
410 : // Since kernel 5.2
411 0 : CPLDebug(
412 : "GDAL",
413 : "CPLIsUserFaultMappingSupported(): syscall(__NR_userfaultfd) "
414 : "failed: "
415 : "insufficient permission. add CAP_SYS_PTRACE capability, or "
416 : "set /proc/sys/vm/unprivileged_userfaultfd to 1");
417 : }
418 : else
419 : {
420 0 : CPLDebug(
421 : "GDAL",
422 : "CPLIsUserFaultMappingSupported(): syscall(__NR_userfaultfd) "
423 : "failed: "
424 : "error = %d",
425 : l_errno);
426 : }
427 0 : nEnableUserFaultFD = false;
428 0 : return false;
429 : }
430 18 : close(uffd);
431 18 : nEnableUserFaultFD = true;
432 18 : return true;
433 : }
434 :
435 : /*
436 : * Returns nullptr on failure, a valid pointer on success.
437 : */
438 3 : cpl_uffd_context *CPLCreateUserFaultMapping(const char *pszFilename,
439 : void **ppVma, uint64_t *pnVmaSize)
440 : {
441 : VSIStatBufL statbuf;
442 3 : struct cpl_uffd_context *ctx = nullptr;
443 :
444 3 : if (!CPLIsUserFaultMappingSupported())
445 : {
446 0 : CPLError(
447 : CE_Failure, CPLE_NotSupported,
448 : "CPLCreateUserFaultMapping(): Linux kernel 4.3 or newer needed");
449 0 : return nullptr;
450 : }
451 :
452 : // Get the size of the asset
453 3 : if (VSIStatL(pszFilename, &statbuf))
454 0 : return nullptr;
455 :
456 : // Setup the `cpl_uffd_context` struct
457 3 : ctx = new cpl_uffd_context();
458 3 : ctx->keep_going = true;
459 3 : ctx->filename = std::string(pszFilename);
460 3 : ctx->page_limit = get_page_limit();
461 3 : ctx->pages_used = 0;
462 3 : ctx->file_size = static_cast<size_t>(statbuf.st_size);
463 3 : ctx->page_size = static_cast<size_t>(std::max(1L, sysconf(_SC_PAGESIZE)));
464 3 : ctx->vma_size = static_cast<size_t>(
465 3 : ((static_cast<vsi_l_offset>(statbuf.st_size) / ctx->page_size) + 1) *
466 3 : ctx->page_size);
467 3 : if (ctx->vma_size < static_cast<vsi_l_offset>(statbuf.st_size))
468 : { // Check for overflow
469 0 : uffd_cleanup(ctx);
470 0 : CPLError(
471 : CE_Failure, CPLE_AppDefined,
472 : "CPLCreateUserFaultMapping(): File too large for architecture");
473 0 : return nullptr;
474 : }
475 :
476 : // If the mmap failed, free resources and return
477 3 : ctx->vma_ptr = mmap(nullptr, ctx->vma_size, PROT_READ,
478 : MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
479 3 : if (ctx->vma_ptr == BAD_MMAP)
480 : {
481 0 : ctx->vma_ptr = nullptr;
482 0 : uffd_cleanup(ctx);
483 0 : CPLError(CE_Failure, CPLE_AppDefined,
484 : "CPLCreateUserFaultMapping(): mmap() failed");
485 0 : return nullptr;
486 : }
487 :
488 : // Attempt to acquire a scratch page to use to fulfill requests.
489 3 : ctx->page_ptr =
490 3 : mmap(nullptr, static_cast<size_t>(ctx->page_size),
491 : PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
492 3 : if (ctx->page_ptr == BAD_MMAP)
493 : {
494 0 : ctx->page_ptr = nullptr;
495 0 : uffd_cleanup(ctx);
496 0 : CPLError(CE_Failure, CPLE_AppDefined,
497 : "CPLCreateUserFaultMapping(): mmap() failed");
498 0 : return nullptr;
499 : }
500 :
501 : // Get userfaultfd
502 :
503 : // Since kernel 5.2, raw userfaultfd is disabled since if the fault
504 : // originates from the kernel, that could lead to easier exploitation of
505 : // kernel bugs. Since kernel 5.11, UFFD_USER_MODE_ONLY can be used to
506 : // restrict the mechanism to faults occurring only from user space, which is
507 : // likely to be our use case.
508 3 : ctx->uffd = static_cast<int>(syscall(
509 : __NR_userfaultfd, O_CLOEXEC | O_NONBLOCK | UFFD_USER_MODE_ONLY));
510 3 : if (ctx->uffd == -1 && errno == EINVAL)
511 0 : ctx->uffd =
512 0 : static_cast<int>(syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK));
513 3 : if (ctx->uffd == -1)
514 : {
515 0 : const int l_errno = errno;
516 0 : ctx->uffd = -1;
517 0 : uffd_cleanup(ctx);
518 0 : if (l_errno == EPERM)
519 : {
520 : // Since kernel 5.2
521 0 : CPLError(
522 : CE_Failure, CPLE_AppDefined,
523 : "CPLCreateUserFaultMapping(): syscall(__NR_userfaultfd) "
524 : "failed: "
525 : "insufficient permission. add CAP_SYS_PTRACE capability, or "
526 : "set /proc/sys/vm/unprivileged_userfaultfd to 1");
527 : }
528 : else
529 : {
530 0 : CPLError(CE_Failure, CPLE_AppDefined,
531 : "CPLCreateUserFaultMapping(): syscall(__NR_userfaultfd) "
532 : "failed: "
533 : "error = %d",
534 : l_errno);
535 : }
536 0 : return nullptr;
537 : }
538 :
539 : // Query API
540 : {
541 3 : struct uffdio_api uffdio_api = {};
542 :
543 3 : uffdio_api.api = UFFD_API;
544 3 : uffdio_api.features = 0;
545 :
546 3 : if (ioctl(ctx->uffd, UFFDIO_API, &uffdio_api) == -1)
547 : {
548 0 : uffd_cleanup(ctx);
549 0 : CPLError(CE_Failure, CPLE_AppDefined,
550 : "CPLCreateUserFaultMapping(): ioctl(UFFDIO_API) failed");
551 0 : return nullptr;
552 : }
553 : }
554 :
555 : // Register memory range
556 3 : ctx->uffdio_register.range.start =
557 3 : reinterpret_cast<uintptr_t>(ctx->vma_ptr);
558 3 : ctx->uffdio_register.range.len = ctx->vma_size;
559 3 : ctx->uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
560 :
561 3 : if (ioctl(ctx->uffd, UFFDIO_REGISTER, &ctx->uffdio_register) == -1)
562 : {
563 0 : uffd_cleanup(ctx);
564 0 : CPLError(CE_Failure, CPLE_AppDefined,
565 : "CPLCreateUserFaultMapping(): ioctl(UFFDIO_REGISTER) failed");
566 0 : return nullptr;
567 : }
568 :
569 : // Start handler thread
570 3 : ctx->thread = CPLCreateJoinableThread(cpl_uffd_fault_handler, ctx);
571 3 : if (ctx->thread == nullptr)
572 : {
573 0 : CPLError(
574 : CE_Failure, CPLE_AppDefined,
575 : "CPLCreateUserFaultMapping(): CPLCreateJoinableThread() failed");
576 0 : uffd_cleanup(ctx);
577 0 : return nullptr;
578 : }
579 :
580 3 : *ppVma = ctx->vma_ptr;
581 3 : *pnVmaSize = ctx->vma_size;
582 3 : return ctx;
583 : }
584 :
585 661 : void CPLDeleteUserFaultMapping(cpl_uffd_context *ctx)
586 : {
587 661 : if (ctx)
588 : {
589 3 : uffd_cleanup(ctx);
590 : }
591 661 : }
592 :
593 : #endif // ENABLE_UFFD
|