From 7802ab88a1afe5c6006896647984c0ed19bdf7cb Mon Sep 17 00:00:00 2001 From: Martin Willi Date: Wed, 15 Apr 2015 16:15:32 +0200 Subject: [PATCH] memory: Move memory manipulation related functions to separate files --- src/libstrongswan/Android.mk | 2 +- src/libstrongswan/Makefile.am | 4 +- src/libstrongswan/utils/utils.c | 219 ----------------------- src/libstrongswan/utils/utils.h | 159 +---------------- src/libstrongswan/utils/utils/memory.c | 237 +++++++++++++++++++++++++ src/libstrongswan/utils/utils/memory.h | 183 +++++++++++++++++++ 6 files changed, 424 insertions(+), 380 deletions(-) create mode 100644 src/libstrongswan/utils/utils/memory.c create mode 100644 src/libstrongswan/utils/utils/memory.h diff --git a/src/libstrongswan/Android.mk b/src/libstrongswan/Android.mk index 8ead333d72..7111218c80 100644 --- a/src/libstrongswan/Android.mk +++ b/src/libstrongswan/Android.mk @@ -40,7 +40,7 @@ settings/settings_parser.c settings/settings_lexer.c utils/cpu_feature.c \ utils/utils.c utils/chunk.c utils/debug.c utils/enum.c utils/identification.c \ utils/lexparser.c utils/optionsfrom.c utils/capabilities.c utils/backtrace.c \ utils/parser_helper.c utils/test.c utils/process.c utils/utils/strerror.c \ -utils/utils/atomics.c utils/utils/string.c +utils/utils/atomics.c utils/utils/string.c utils/utils/memory.c libstrongswan_la_SOURCES += \ threading/thread.c \ diff --git a/src/libstrongswan/Makefile.am b/src/libstrongswan/Makefile.am index dda4a88007..95e2b8282e 100644 --- a/src/libstrongswan/Makefile.am +++ b/src/libstrongswan/Makefile.am @@ -38,7 +38,7 @@ settings/settings_parser.y settings/settings_lexer.l utils/cpu_feature.c \ utils/utils.c utils/chunk.c utils/debug.c utils/enum.c utils/identification.c \ utils/lexparser.c utils/optionsfrom.c utils/capabilities.c utils/backtrace.c \ utils/parser_helper.c utils/test.c utils/process.c utils/utils/strerror.c \ -utils/utils/atomics.c utils/utils/string.c +utils/utils/atomics.c utils/utils/string.c utils/utils/memory.c if !USE_WINDOWS libstrongswan_la_SOURCES += \ @@ -109,7 +109,7 @@ utils/printf_hook/printf_hook_vstr.h utils/printf_hook/printf_hook_builtin.h \ utils/parser_helper.h utils/test.h utils/integrity_checker.h utils/process.h \ utils/utils/strerror.h utils/compat/windows.h utils/compat/apple.h \ utils/utils/atomics.h utils/utils/types.h utils/utils/byteorder.h \ -utils/utils/string.h +utils/utils/string.h utils/utils/memory.h endif library.lo : $(top_builddir)/config.status diff --git a/src/libstrongswan/utils/utils.c b/src/libstrongswan/utils/utils.c index 14eff33e2c..497e0516e1 100644 --- a/src/libstrongswan/utils/utils.c +++ b/src/libstrongswan/utils/utils.c @@ -101,120 +101,6 @@ void free_align(void *ptr) free(ptr - pad); } -/** - * Described in header. - */ -void memxor(u_int8_t dst[], u_int8_t src[], size_t n) -{ - int m, i; - - /* byte wise XOR until dst aligned */ - for (i = 0; (uintptr_t)&dst[i] % sizeof(long) && i < n; i++) - { - dst[i] ^= src[i]; - } - /* try to use words if src shares an aligment with dst */ - switch (((uintptr_t)&src[i] % sizeof(long))) - { - case 0: - for (m = n - sizeof(long); i <= m; i += sizeof(long)) - { - *(long*)&dst[i] ^= *(long*)&src[i]; - } - break; - case sizeof(int): - for (m = n - sizeof(int); i <= m; i += sizeof(int)) - { - *(int*)&dst[i] ^= *(int*)&src[i]; - } - break; - case sizeof(short): - for (m = n - sizeof(short); i <= m; i += sizeof(short)) - { - *(short*)&dst[i] ^= *(short*)&src[i]; - } - break; - default: - break; - } - /* byte wise XOR of the rest */ - for (; i < n; i++) - { - dst[i] ^= src[i]; - } -} - -/** - * Described in header. - */ -void memwipe_noinline(void *ptr, size_t n) -{ - memwipe_inline(ptr, n); -} - -/** - * Described in header. - */ -bool memeq_const(const void *x, const void *y, size_t len) -{ - const u_char *a, *b; - u_int bad = 0; - size_t i; - - a = (const u_char*)x; - b = (const u_char*)y; - - for (i = 0; i < len; i++) - { - bad |= a[i] != b[i]; - } - return !bad; -} - -/** - * Described in header. - */ -void *memstr(const void *haystack, const char *needle, size_t n) -{ - const u_char *pos = haystack; - size_t l; - - if (!haystack || !needle || (l = strlen(needle)) == 0) - { - return NULL; - } - for (; n >= l; ++pos, --n) - { - if (memeq(pos, needle, l)) - { - return (void*)pos; - } - } - return NULL; -} - -/** - * Described in header. - */ -void *utils_memrchr(const void *s, int c, size_t n) -{ - const u_char *pos; - - if (!s || !n) - { - return NULL; - } - - for (pos = s + n - 1; pos >= (u_char*)s; pos--) - { - if (*pos == (u_char)c) - { - return (void*)pos; - } - } - return NULL; -} - #ifdef WIN32 /** @@ -649,50 +535,6 @@ void nop() { } -#ifdef HAVE_FMEMOPEN_FALLBACK - -static int fmemread(chunk_t *cookie, char *buf, int size) -{ - int len; - - len = min(size, cookie->len); - memcpy(buf, cookie->ptr, len); - *cookie = chunk_skip(*cookie, len); - - return len; -} - -static int fmemwrite(chunk_t *cookie, const char *buf, int size) -{ - int len; - - len = min(size, cookie->len); - memcpy(cookie->ptr, buf, len); - *cookie = chunk_skip(*cookie, len); - - return len; -} - -static int fmemclose(void *cookie) -{ - free(cookie); - return 0; -} - -FILE *fmemopen(void *buf, size_t size, const char *mode) -{ - chunk_t *cookie; - - INIT(cookie, - .ptr = buf, - .len = size, - ); - - return funopen(cookie, (void*)fmemread, (void*)fmemwrite, NULL, fmemclose); -} - -#endif /* FMEMOPEN fallback*/ - /** * See header */ @@ -781,64 +623,3 @@ int time_delta_printf_hook(printf_hook_data_t *data, printf_hook_spec_t *spec, return print_in_hook(data, "%" PRIu64 " %s%s", delta, unit, (delta == 1) ? "" : "s"); } - -/** - * Number of bytes per line to dump raw data - */ -#define BYTES_PER_LINE 16 - -static char hexdig_upper[] = "0123456789ABCDEF"; - -/** - * Described in header. - */ -int mem_printf_hook(printf_hook_data_t *data, - printf_hook_spec_t *spec, const void *const *args) -{ - char *bytes = *((void**)(args[0])); - u_int len = *((int*)(args[1])); - - char buffer[BYTES_PER_LINE * 3]; - char ascii_buffer[BYTES_PER_LINE + 1]; - char *buffer_pos = buffer; - char *bytes_pos = bytes; - char *bytes_roof = bytes + len; - int line_start = 0; - int i = 0; - int written = 0; - - written += print_in_hook(data, "=> %u bytes @ %p", len, bytes); - - while (bytes_pos < bytes_roof) - { - *buffer_pos++ = hexdig_upper[(*bytes_pos >> 4) & 0xF]; - *buffer_pos++ = hexdig_upper[ *bytes_pos & 0xF]; - - ascii_buffer[i++] = - (*bytes_pos > 31 && *bytes_pos < 127) ? *bytes_pos : '.'; - - if (++bytes_pos == bytes_roof || i == BYTES_PER_LINE) - { - int padding = 3 * (BYTES_PER_LINE - i); - - while (padding--) - { - *buffer_pos++ = ' '; - } - *buffer_pos++ = '\0'; - ascii_buffer[i] = '\0'; - - written += print_in_hook(data, "\n%4d: %s %s", - line_start, buffer, ascii_buffer); - - buffer_pos = buffer; - line_start += BYTES_PER_LINE; - i = 0; - } - else - { - *buffer_pos++ = ' '; - } - } - return written; -} diff --git a/src/libstrongswan/utils/utils.h b/src/libstrongswan/utils/utils.h index 138d5e9952..19d5a5f61f 100644 --- a/src/libstrongswan/utils/utils.h +++ b/src/libstrongswan/utils/utils.h @@ -79,6 +79,7 @@ #include "utils/atomics.h" #include "utils/byteorder.h" #include "utils/string.h" +#include "utils/memory.h" #include "utils/strerror.h" #ifdef __APPLE__ # include "compat/apple.h" @@ -103,61 +104,6 @@ void utils_init(); */ void utils_deinit(); -/** - * Helper function that compares two binary blobs for equality - */ -static inline bool memeq(const void *x, const void *y, size_t len) -{ - return memcmp(x, y, len) == 0; -} - -/** - * Same as memeq(), but with a constant runtime, safe for cryptographic use. - */ -bool memeq_const(const void *x, const void *y, size_t len); - -/** - * Calling memcpy() with NULL pointers, even with n == 0, results in undefined - * behavior according to the C standard. This version is guaranteed to not - * access the pointers if n is 0. - */ -static inline void *memcpy_noop(void *dst, const void *src, size_t n) -{ - return n ? memcpy(dst, src, n) : dst; -} -#ifdef memcpy -# undef memcpy -#endif -#define memcpy(d,s,n) memcpy_noop(d,s,n) - -/** - * Calling memmove() with NULL pointers, even with n == 0, results in undefined - * behavior according to the C standard. This version is guaranteed to not - * access the pointers if n is 0. - */ -static inline void *memmove_noop(void *dst, const void *src, size_t n) -{ - return n ? memmove(dst, src, n) : dst; -} -#ifdef memmove -# undef memmove -#endif -#define memmove(d,s,n) memmove_noop(d,s,n) - -/** - * Calling memset() with a NULL pointer, even with n == 0, results in undefined - * behavior according to the C standard. This version is guaranteed to not - * access the pointer if n is 0. - */ -static inline void *memset_noop(void *s, int c, size_t n) -{ - return n ? memset(s, c, n) : s; -} -#ifdef memset -# undef memset -#endif -#define memset(s,c,n) memset_noop(s,c,n) - /** * Macro gives back larger of two values. */ @@ -477,83 +423,6 @@ void* malloc_align(size_t size, u_int8_t align); */ void free_align(void *ptr); -/** - * Same as memcpy, but XORs src into dst instead of copy - */ -void memxor(u_int8_t dest[], u_int8_t src[], size_t n); - -/** - * Safely overwrite n bytes of memory at ptr with zero, non-inlining variant. - */ -void memwipe_noinline(void *ptr, size_t n); - -/** - * Safely overwrite n bytes of memory at ptr with zero, inlining variant. - */ -static inline void memwipe_inline(void *ptr, size_t n) -{ - volatile char *c = (volatile char*)ptr; - size_t m, i; - - /* byte wise until long aligned */ - for (i = 0; (uintptr_t)&c[i] % sizeof(long) && i < n; i++) - { - c[i] = 0; - } - /* word wise */ - if (n >= sizeof(long)) - { - for (m = n - sizeof(long); i <= m; i += sizeof(long)) - { - *(volatile long*)&c[i] = 0; - } - } - /* byte wise of the rest */ - for (; i < n; i++) - { - c[i] = 0; - } -} - -/** - * Safely overwrite n bytes of memory at ptr with zero, auto-inlining variant. - */ -static inline void memwipe(void *ptr, size_t n) -{ - if (!ptr) - { - return; - } - if (__builtin_constant_p(n)) - { - memwipe_inline(ptr, n); - } - else - { - memwipe_noinline(ptr, n); - } -} - -/** - * A variant of strstr with the characteristics of memchr, where haystack is not - * a null-terminated string but simply a memory area of length n. - */ -void *memstr(const void *haystack, const char *needle, size_t n); - -/** - * Replacement for memrchr(3) if it is not provided by the C library. - * - * @param s start of the memory area to search - * @param c character to search - * @param n length of memory area to search - * @return pointer to the found character or NULL - */ -void *utils_memrchr(const void *s, int c, size_t n); - -#ifndef HAVE_MEMRCHR -#define memrchr(s,c,n) utils_memrchr(s,c,n) -#endif - /** * Portable function to wait for SIGINT/SIGTERM (or equivalent). */ @@ -696,23 +565,6 @@ static inline size_t round_down(size_t size, size_t alignment) return size - (size % alignment); } -#ifndef HAVE_FMEMOPEN -# ifdef HAVE_FUNOPEN -# define HAVE_FMEMOPEN -# define HAVE_FMEMOPEN_FALLBACK -# include -/** - * fmemopen(3) fallback using BSD funopen. - * - * We could also provide one using fopencookie(), but should we have it we - * most likely have fmemopen(). - * - * fseek() is currently not supported. - */ -FILE *fmemopen(void *buf, size_t size, const char *mode); -# endif /* FUNOPEN */ -#endif /* FMEMOPEN */ - /** * printf hook for time_t. * @@ -731,13 +583,4 @@ int time_printf_hook(printf_hook_data_t *data, printf_hook_spec_t *spec, int time_delta_printf_hook(printf_hook_data_t *data, printf_hook_spec_t *spec, const void *const *args); -/** - * printf hook for memory areas. - * - * Arguments are: - * u_char *ptr, u_int len - */ -int mem_printf_hook(printf_hook_data_t *data, printf_hook_spec_t *spec, - const void *const *args); - #endif /** UTILS_H_ @}*/ diff --git a/src/libstrongswan/utils/utils/memory.c b/src/libstrongswan/utils/utils/memory.c new file mode 100644 index 0000000000..a15371518b --- /dev/null +++ b/src/libstrongswan/utils/utils/memory.c @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2008-2014 Tobias Brunner + * Copyright (C) 2005-2008 Martin Willi + * Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See . + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include +#include + +/** + * Described in header. + */ +void memxor(u_int8_t dst[], u_int8_t src[], size_t n) +{ + int m, i; + + /* byte wise XOR until dst aligned */ + for (i = 0; (uintptr_t)&dst[i] % sizeof(long) && i < n; i++) + { + dst[i] ^= src[i]; + } + /* try to use words if src shares an aligment with dst */ + switch (((uintptr_t)&src[i] % sizeof(long))) + { + case 0: + for (m = n - sizeof(long); i <= m; i += sizeof(long)) + { + *(long*)&dst[i] ^= *(long*)&src[i]; + } + break; + case sizeof(int): + for (m = n - sizeof(int); i <= m; i += sizeof(int)) + { + *(int*)&dst[i] ^= *(int*)&src[i]; + } + break; + case sizeof(short): + for (m = n - sizeof(short); i <= m; i += sizeof(short)) + { + *(short*)&dst[i] ^= *(short*)&src[i]; + } + break; + default: + break; + } + /* byte wise XOR of the rest */ + for (; i < n; i++) + { + dst[i] ^= src[i]; + } +} + +/** + * Described in header. + */ +void memwipe_noinline(void *ptr, size_t n) +{ + memwipe_inline(ptr, n); +} + +/** + * Described in header. + */ +bool memeq_const(const void *x, const void *y, size_t len) +{ + const u_char *a, *b; + u_int bad = 0; + size_t i; + + a = (const u_char*)x; + b = (const u_char*)y; + + for (i = 0; i < len; i++) + { + bad |= a[i] != b[i]; + } + return !bad; +} + +/** + * Described in header. + */ +void *memstr(const void *haystack, const char *needle, size_t n) +{ + const u_char *pos = haystack; + size_t l; + + if (!haystack || !needle || (l = strlen(needle)) == 0) + { + return NULL; + } + for (; n >= l; ++pos, --n) + { + if (memeq(pos, needle, l)) + { + return (void*)pos; + } + } + return NULL; +} + +/** + * Described in header. + */ +void *utils_memrchr(const void *s, int c, size_t n) +{ + const u_char *pos; + + if (!s || !n) + { + return NULL; + } + + for (pos = s + n - 1; pos >= (u_char*)s; pos--) + { + if (*pos == (u_char)c) + { + return (void*)pos; + } + } + return NULL; +} + +#ifdef HAVE_FMEMOPEN_FALLBACK + +static int fmemread(chunk_t *cookie, char *buf, int size) +{ + int len; + + len = min(size, cookie->len); + memcpy(buf, cookie->ptr, len); + *cookie = chunk_skip(*cookie, len); + + return len; +} + +static int fmemwrite(chunk_t *cookie, const char *buf, int size) +{ + int len; + + len = min(size, cookie->len); + memcpy(cookie->ptr, buf, len); + *cookie = chunk_skip(*cookie, len); + + return len; +} + +static int fmemclose(void *cookie) +{ + free(cookie); + return 0; +} + +FILE *fmemopen(void *buf, size_t size, const char *mode) +{ + chunk_t *cookie; + + INIT(cookie, + .ptr = buf, + .len = size, + ); + + return funopen(cookie, (void*)fmemread, (void*)fmemwrite, NULL, fmemclose); +} + +#endif /* FMEMOPEN fallback*/ + +/** + * Number of bytes per line to dump raw data + */ +#define BYTES_PER_LINE 16 + +static char hexdig_upper[] = "0123456789ABCDEF"; + +/** + * Described in header. + */ +int mem_printf_hook(printf_hook_data_t *data, + printf_hook_spec_t *spec, const void *const *args) +{ + char *bytes = *((void**)(args[0])); + u_int len = *((int*)(args[1])); + + char buffer[BYTES_PER_LINE * 3]; + char ascii_buffer[BYTES_PER_LINE + 1]; + char *buffer_pos = buffer; + char *bytes_pos = bytes; + char *bytes_roof = bytes + len; + int line_start = 0; + int i = 0; + int written = 0; + + written += print_in_hook(data, "=> %u bytes @ %p", len, bytes); + + while (bytes_pos < bytes_roof) + { + *buffer_pos++ = hexdig_upper[(*bytes_pos >> 4) & 0xF]; + *buffer_pos++ = hexdig_upper[ *bytes_pos & 0xF]; + + ascii_buffer[i++] = + (*bytes_pos > 31 && *bytes_pos < 127) ? *bytes_pos : '.'; + + if (++bytes_pos == bytes_roof || i == BYTES_PER_LINE) + { + int padding = 3 * (BYTES_PER_LINE - i); + + while (padding--) + { + *buffer_pos++ = ' '; + } + *buffer_pos++ = '\0'; + ascii_buffer[i] = '\0'; + + written += print_in_hook(data, "\n%4d: %s %s", + line_start, buffer, ascii_buffer); + + buffer_pos = buffer; + line_start += BYTES_PER_LINE; + i = 0; + } + else + { + *buffer_pos++ = ' '; + } + } + return written; +} diff --git a/src/libstrongswan/utils/utils/memory.h b/src/libstrongswan/utils/utils/memory.h new file mode 100644 index 0000000000..aef318f6c0 --- /dev/null +++ b/src/libstrongswan/utils/utils/memory.h @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2008-2014 Tobias Brunner + * Copyright (C) 2008 Martin Willi + * Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See . + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +/** + * @defgroup memory_i memory + * @{ @ingroup utils_i + */ + +#ifndef MEMORY_H_ +#define MEMORY_H_ + +/** + * Helper function that compares two binary blobs for equality + */ +static inline bool memeq(const void *x, const void *y, size_t len) +{ + return memcmp(x, y, len) == 0; +} + +/** + * Same as memeq(), but with a constant runtime, safe for cryptographic use. + */ +bool memeq_const(const void *x, const void *y, size_t len); + +/** + * Calling memcpy() with NULL pointers, even with n == 0, results in undefined + * behavior according to the C standard. This version is guaranteed to not + * access the pointers if n is 0. + */ +static inline void *memcpy_noop(void *dst, const void *src, size_t n) +{ + return n ? memcpy(dst, src, n) : dst; +} +#ifdef memcpy +# undef memcpy +#endif +#define memcpy(d,s,n) memcpy_noop(d,s,n) + +/** + * Calling memmove() with NULL pointers, even with n == 0, results in undefined + * behavior according to the C standard. This version is guaranteed to not + * access the pointers if n is 0. + */ +static inline void *memmove_noop(void *dst, const void *src, size_t n) +{ + return n ? memmove(dst, src, n) : dst; +} +#ifdef memmove +# undef memmove +#endif +#define memmove(d,s,n) memmove_noop(d,s,n) + +/** + * Calling memset() with a NULL pointer, even with n == 0, results in undefined + * behavior according to the C standard. This version is guaranteed to not + * access the pointer if n is 0. + */ +static inline void *memset_noop(void *s, int c, size_t n) +{ + return n ? memset(s, c, n) : s; +} +#ifdef memset +# undef memset +#endif +#define memset(s,c,n) memset_noop(s,c,n) + +/** + * Same as memcpy, but XORs src into dst instead of copy + */ +void memxor(u_int8_t dest[], u_int8_t src[], size_t n); + +/** + * Safely overwrite n bytes of memory at ptr with zero, non-inlining variant. + */ +void memwipe_noinline(void *ptr, size_t n); + +/** + * Safely overwrite n bytes of memory at ptr with zero, inlining variant. + */ +static inline void memwipe_inline(void *ptr, size_t n) +{ + volatile char *c = (volatile char*)ptr; + size_t m, i; + + /* byte wise until long aligned */ + for (i = 0; (uintptr_t)&c[i] % sizeof(long) && i < n; i++) + { + c[i] = 0; + } + /* word wise */ + if (n >= sizeof(long)) + { + for (m = n - sizeof(long); i <= m; i += sizeof(long)) + { + *(volatile long*)&c[i] = 0; + } + } + /* byte wise of the rest */ + for (; i < n; i++) + { + c[i] = 0; + } +} + +/** + * Safely overwrite n bytes of memory at ptr with zero, auto-inlining variant. + */ +static inline void memwipe(void *ptr, size_t n) +{ + if (!ptr) + { + return; + } + if (__builtin_constant_p(n)) + { + memwipe_inline(ptr, n); + } + else + { + memwipe_noinline(ptr, n); + } +} + +/** + * A variant of strstr with the characteristics of memchr, where haystack is not + * a null-terminated string but simply a memory area of length n. + */ +void *memstr(const void *haystack, const char *needle, size_t n); + +/** + * Replacement for memrchr(3) if it is not provided by the C library. + * + * @param s start of the memory area to search + * @param c character to search + * @param n length of memory area to search + * @return pointer to the found character or NULL + */ +void *utils_memrchr(const void *s, int c, size_t n); + +#ifndef HAVE_MEMRCHR +#define memrchr(s,c,n) utils_memrchr(s,c,n) +#endif + +#ifndef HAVE_FMEMOPEN +# ifdef HAVE_FUNOPEN +# define HAVE_FMEMOPEN +# define HAVE_FMEMOPEN_FALLBACK +# include +/** + * fmemopen(3) fallback using BSD funopen. + * + * We could also provide one using fopencookie(), but should we have it we + * most likely have fmemopen(). + * + * fseek() is currently not supported. + */ +FILE *fmemopen(void *buf, size_t size, const char *mode); +# endif /* FUNOPEN */ +#endif /* FMEMOPEN */ + +/** + * printf hook for memory areas. + * + * Arguments are: + * u_char *ptr, u_int len + */ +int mem_printf_hook(printf_hook_data_t *data, printf_hook_spec_t *spec, + const void *const *args); + +#endif /** MEMORY_H_ @} */ -- 2.47.2