Merge tag 'hardening-v5.15-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/kees/linux

Pull hardening updates from Kees Cook:

 - Expand lib/test_stackinit to include more initialization styles

 - Improve Kconfig for CLang's auto-var-init feature

 - Introduce support for GCC's zero-call-used-regs feature

* tag 'hardening-v5.15-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/kees/linux:
  lib/test_stackinit: Add assigned initializers
  lib/test_stackinit: Allow building stand-alone
  lib/test_stackinit: Fix static initializer test
  hardening: Clarify Kconfig text for auto-var-init
  hardening: Introduce CONFIG_ZERO_CALL_USED_REGS
This commit is contained in:
Linus Torvalds
2021-09-02 12:35:34 -07:00
3 changed files with 241 additions and 86 deletions

View File

@ -841,6 +841,11 @@ endif
# for the randomize_kstack_offset feature. Disable it for all compilers. # for the randomize_kstack_offset feature. Disable it for all compilers.
KBUILD_CFLAGS += $(call cc-option, -fno-stack-clash-protection) KBUILD_CFLAGS += $(call cc-option, -fno-stack-clash-protection)
# Clear used registers at func exit (to reduce data lifetime and ROP gadgets).
ifdef CONFIG_ZERO_CALL_USED_REGS
KBUILD_CFLAGS += -fzero-call-used-regs=used-gpr
endif
DEBUG_CFLAGS := DEBUG_CFLAGS :=
# Workaround for GCC versions < 5.0 # Workaround for GCC versions < 5.0

View File

@ -1,8 +1,13 @@
// SPDX-License-Identifier: GPL-2.0 // SPDX-License-Identifier: GPL-2.0-or-later
/* /*
* Test cases for compiler-based stack variable zeroing via future * Test cases for compiler-based stack variable zeroing via
* compiler flags or CONFIG_GCC_PLUGIN_STRUCTLEAK*. * -ftrivial-auto-var-init={zero,pattern} or CONFIG_GCC_PLUGIN_STRUCTLEAK*.
*
* External build example:
* clang -O2 -Wall -ftrivial-auto-var-init=pattern \
* -o test_stackinit test_stackinit.c
*/ */
#ifdef __KERNEL__
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/init.h> #include <linux/init.h>
@ -10,6 +15,63 @@
#include <linux/module.h> #include <linux/module.h>
#include <linux/string.h> #include <linux/string.h>
#else
/* Userspace headers. */
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <errno.h>
#include <sys/types.h>
/* Linux kernel-ism stubs for stand-alone userspace build. */
#define KBUILD_MODNAME "stackinit"
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#define pr_err(fmt, ...) fprintf(stderr, pr_fmt(fmt), ##__VA_ARGS__)
#define pr_warn(fmt, ...) fprintf(stderr, pr_fmt(fmt), ##__VA_ARGS__)
#define pr_info(fmt, ...) fprintf(stdout, pr_fmt(fmt), ##__VA_ARGS__)
#define __init /**/
#define __exit /**/
#define __user /**/
#define noinline __attribute__((__noinline__))
#define __aligned(x) __attribute__((__aligned__(x)))
#ifdef __clang__
# define __compiletime_error(message) /**/
#else
# define __compiletime_error(message) __attribute__((__error__(message)))
#endif
#define __compiletime_assert(condition, msg, prefix, suffix) \
do { \
extern void prefix ## suffix(void) __compiletime_error(msg); \
if (!(condition)) \
prefix ## suffix(); \
} while (0)
#define _compiletime_assert(condition, msg, prefix, suffix) \
__compiletime_assert(condition, msg, prefix, suffix)
#define compiletime_assert(condition, msg) \
_compiletime_assert(condition, msg, __compiletime_assert_, __COUNTER__)
#define BUILD_BUG_ON_MSG(cond, msg) compiletime_assert(!(cond), msg)
#define BUILD_BUG_ON(condition) \
BUILD_BUG_ON_MSG(condition, "BUILD_BUG_ON failed: " #condition)
typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
typedef uint64_t u64;
#define module_init(func) static int (*do_init)(void) = func
#define module_exit(func) static void (*do_exit)(void) = func
#define MODULE_LICENSE(str) int main(void) { \
int rc; \
/* License: str */ \
rc = do_init(); \
if (rc == 0) \
do_exit(); \
return rc; \
}
#endif /* __KERNEL__ */
/* Exfiltration buffer. */ /* Exfiltration buffer. */
#define MAX_VAR_SIZE 128 #define MAX_VAR_SIZE 128
static u8 check_buf[MAX_VAR_SIZE]; static u8 check_buf[MAX_VAR_SIZE];
@ -33,6 +95,10 @@ static bool range_contains(char *haystack_start, size_t haystack_size,
return false; return false;
} }
/* Whether the test is expected to fail. */
#define WANT_SUCCESS 0
#define XFAIL 1
#define DO_NOTHING_TYPE_SCALAR(var_type) var_type #define DO_NOTHING_TYPE_SCALAR(var_type) var_type
#define DO_NOTHING_TYPE_STRING(var_type) void #define DO_NOTHING_TYPE_STRING(var_type) void
#define DO_NOTHING_TYPE_STRUCT(var_type) void #define DO_NOTHING_TYPE_STRUCT(var_type) void
@ -58,34 +124,73 @@ static bool range_contains(char *haystack_start, size_t haystack_size,
#define INIT_CLONE_STRING [FILL_SIZE_STRING] #define INIT_CLONE_STRING [FILL_SIZE_STRING]
#define INIT_CLONE_STRUCT /**/ #define INIT_CLONE_STRUCT /**/
#define INIT_SCALAR_none /**/ #define ZERO_CLONE_SCALAR(zero) memset(&(zero), 0x00, sizeof(zero))
#define INIT_SCALAR_zero = 0 #define ZERO_CLONE_STRING(zero) memset(&(zero), 0x00, sizeof(zero))
/*
* For the struct, intentionally poison padding to see if it gets
* copied out in direct assignments.
* */
#define ZERO_CLONE_STRUCT(zero) \
do { \
memset(&(zero), 0xFF, sizeof(zero)); \
zero.one = 0; \
zero.two = 0; \
zero.three = 0; \
zero.four = 0; \
} while (0)
#define INIT_STRING_none [FILL_SIZE_STRING] /**/ #define INIT_SCALAR_none(var_type) /**/
#define INIT_STRING_zero [FILL_SIZE_STRING] = { } #define INIT_SCALAR_zero(var_type) = 0
#define INIT_STRUCT_none /**/ #define INIT_STRING_none(var_type) [FILL_SIZE_STRING] /**/
#define INIT_STRUCT_zero = { } #define INIT_STRING_zero(var_type) [FILL_SIZE_STRING] = { }
#define INIT_STRUCT_static_partial = { .two = 0, }
#define INIT_STRUCT_static_all = { .one = arg->one, \ #define INIT_STRUCT_none(var_type) /**/
.two = arg->two, \ #define INIT_STRUCT_zero(var_type) = { }
.three = arg->three, \
.four = arg->four, \
#define __static_partial { .two = 0, }
#define __static_all { .one = 0, \
.two = 0, \
.three = 0, \
.four = 0, \
} }
#define INIT_STRUCT_dynamic_partial = { .two = arg->two, } #define __dynamic_partial { .two = arg->two, }
#define INIT_STRUCT_dynamic_all = { .one = arg->one, \ #define __dynamic_all { .one = arg->one, \
.two = arg->two, \ .two = arg->two, \
.three = arg->three, \ .three = arg->three, \
.four = arg->four, \ .four = arg->four, \
} }
#define INIT_STRUCT_runtime_partial ; \ #define __runtime_partial var.two = 0
var.two = 0 #define __runtime_all var.one = 0; \
#define INIT_STRUCT_runtime_all ; \
var.one = 0; \
var.two = 0; \ var.two = 0; \
var.three = 0; \ var.three = 0; \
memset(&var.four, 0, \ var.four = 0
sizeof(var.four))
#define INIT_STRUCT_static_partial(var_type) \
= __static_partial
#define INIT_STRUCT_static_all(var_type) \
= __static_all
#define INIT_STRUCT_dynamic_partial(var_type) \
= __dynamic_partial
#define INIT_STRUCT_dynamic_all(var_type) \
= __dynamic_all
#define INIT_STRUCT_runtime_partial(var_type) \
; __runtime_partial
#define INIT_STRUCT_runtime_all(var_type) \
; __runtime_all
#define INIT_STRUCT_assigned_static_partial(var_type) \
; var = (var_type)__static_partial
#define INIT_STRUCT_assigned_static_all(var_type) \
; var = (var_type)__static_all
#define INIT_STRUCT_assigned_dynamic_partial(var_type) \
; var = (var_type)__dynamic_partial
#define INIT_STRUCT_assigned_dynamic_all(var_type) \
; var = (var_type)__dynamic_all
#define INIT_STRUCT_assigned_copy(var_type) \
; var = *(arg)
/* /*
* @name: unique string name for the test * @name: unique string name for the test
@ -106,7 +211,7 @@ static noinline __init int test_ ## name (void) \
BUILD_BUG_ON(sizeof(zero) > MAX_VAR_SIZE); \ BUILD_BUG_ON(sizeof(zero) > MAX_VAR_SIZE); \
\ \
/* Fill clone type with zero for per-field init. */ \ /* Fill clone type with zero for per-field init. */ \
memset(&zero, 0x00, sizeof(zero)); \ ZERO_CLONE_ ## which(zero); \
/* Clear entire check buffer for 0xFF overlap test. */ \ /* Clear entire check buffer for 0xFF overlap test. */ \
memset(check_buf, 0x00, sizeof(check_buf)); \ memset(check_buf, 0x00, sizeof(check_buf)); \
/* Fill stack with 0xFF. */ \ /* Fill stack with 0xFF. */ \
@ -149,7 +254,7 @@ static noinline __init int test_ ## name (void) \
return (xfail) ? 0 : 1; \ return (xfail) ? 0 : 1; \
} \ } \
} }
#define DEFINE_TEST(name, var_type, which, init_level) \ #define DEFINE_TEST(name, var_type, which, init_level, xfail) \
/* no-op to force compiler into ignoring "uninitialized" vars */\ /* no-op to force compiler into ignoring "uninitialized" vars */\
static noinline __init DO_NOTHING_TYPE_ ## which(var_type) \ static noinline __init DO_NOTHING_TYPE_ ## which(var_type) \
do_nothing_ ## name(var_type *ptr) \ do_nothing_ ## name(var_type *ptr) \
@ -165,7 +270,8 @@ static noinline __init int leaf_ ## name(unsigned long sp, \
var_type *arg) \ var_type *arg) \
{ \ { \
char buf[VAR_BUFFER]; \ char buf[VAR_BUFFER]; \
var_type var INIT_ ## which ## _ ## init_level; \ var_type var \
INIT_ ## which ## _ ## init_level(var_type); \
\ \
target_start = &var; \ target_start = &var; \
target_size = sizeof(var); \ target_size = sizeof(var); \
@ -191,7 +297,7 @@ static noinline __init int leaf_ ## name(unsigned long sp, \
\ \
return (int)buf[0] | (int)buf[sizeof(buf) - 1]; \ return (int)buf[0] | (int)buf[sizeof(buf) - 1]; \
} \ } \
DEFINE_TEST_DRIVER(name, var_type, which, 0) DEFINE_TEST_DRIVER(name, var_type, which, xfail)
/* Structure with no padding. */ /* Structure with no padding. */
struct test_packed { struct test_packed {
@ -210,18 +316,13 @@ struct test_small_hole {
unsigned long four; unsigned long four;
}; };
/* Try to trigger unhandled padding in a structure. */ /* Trigger unhandled padding in a structure. */
struct test_aligned {
u32 internal1;
u64 internal2;
} __aligned(64);
struct test_big_hole { struct test_big_hole {
u8 one; u8 one;
u8 two; u8 two;
u8 three; u8 three;
/* 61 byte padding hole here. */ /* 61 byte padding hole here. */
struct test_aligned four; u8 four __aligned(64);
} __aligned(64); } __aligned(64);
struct test_trailing_hole { struct test_trailing_hole {
@ -240,42 +341,50 @@ struct test_user {
unsigned long four; unsigned long four;
}; };
#define DEFINE_SCALAR_TEST(name, init) \ #define DEFINE_SCALAR_TEST(name, init, xfail) \
DEFINE_TEST(name ## _ ## init, name, SCALAR, init) DEFINE_TEST(name ## _ ## init, name, SCALAR, \
init, xfail)
#define DEFINE_SCALAR_TESTS(init) \ #define DEFINE_SCALAR_TESTS(init, xfail) \
DEFINE_SCALAR_TEST(u8, init); \ DEFINE_SCALAR_TEST(u8, init, xfail); \
DEFINE_SCALAR_TEST(u16, init); \ DEFINE_SCALAR_TEST(u16, init, xfail); \
DEFINE_SCALAR_TEST(u32, init); \ DEFINE_SCALAR_TEST(u32, init, xfail); \
DEFINE_SCALAR_TEST(u64, init); \ DEFINE_SCALAR_TEST(u64, init, xfail); \
DEFINE_TEST(char_array_ ## init, unsigned char, STRING, init) DEFINE_TEST(char_array_ ## init, unsigned char, \
STRING, init, xfail)
#define DEFINE_STRUCT_TEST(name, init) \ #define DEFINE_STRUCT_TEST(name, init, xfail) \
DEFINE_TEST(name ## _ ## init, \ DEFINE_TEST(name ## _ ## init, \
struct test_ ## name, STRUCT, init) struct test_ ## name, STRUCT, init, \
xfail)
#define DEFINE_STRUCT_TESTS(init) \ #define DEFINE_STRUCT_TESTS(init, xfail) \
DEFINE_STRUCT_TEST(small_hole, init); \ DEFINE_STRUCT_TEST(small_hole, init, xfail); \
DEFINE_STRUCT_TEST(big_hole, init); \ DEFINE_STRUCT_TEST(big_hole, init, xfail); \
DEFINE_STRUCT_TEST(trailing_hole, init); \ DEFINE_STRUCT_TEST(trailing_hole, init, xfail); \
DEFINE_STRUCT_TEST(packed, init) DEFINE_STRUCT_TEST(packed, init, xfail)
#define DEFINE_STRUCT_INITIALIZER_TESTS(base) \
DEFINE_STRUCT_TESTS(base ## _ ## partial, \
WANT_SUCCESS); \
DEFINE_STRUCT_TESTS(base ## _ ## all, \
WANT_SUCCESS)
/* These should be fully initialized all the time! */ /* These should be fully initialized all the time! */
DEFINE_SCALAR_TESTS(zero); DEFINE_SCALAR_TESTS(zero, WANT_SUCCESS);
DEFINE_STRUCT_TESTS(zero); DEFINE_STRUCT_TESTS(zero, WANT_SUCCESS);
/* Static initialization: padding may be left uninitialized. */ /* Struct initializers: padding may be left uninitialized. */
DEFINE_STRUCT_TESTS(static_partial); DEFINE_STRUCT_INITIALIZER_TESTS(static);
DEFINE_STRUCT_TESTS(static_all); DEFINE_STRUCT_INITIALIZER_TESTS(dynamic);
/* Dynamic initialization: padding may be left uninitialized. */ DEFINE_STRUCT_INITIALIZER_TESTS(runtime);
DEFINE_STRUCT_TESTS(dynamic_partial); DEFINE_STRUCT_INITIALIZER_TESTS(assigned_static);
DEFINE_STRUCT_TESTS(dynamic_all); DEFINE_STRUCT_INITIALIZER_TESTS(assigned_dynamic);
/* Runtime initialization: padding may be left uninitialized. */ DEFINE_STRUCT_TESTS(assigned_copy, XFAIL);
DEFINE_STRUCT_TESTS(runtime_partial);
DEFINE_STRUCT_TESTS(runtime_all);
/* No initialization without compiler instrumentation. */ /* No initialization without compiler instrumentation. */
DEFINE_SCALAR_TESTS(none); DEFINE_SCALAR_TESTS(none, WANT_SUCCESS);
DEFINE_STRUCT_TESTS(none); DEFINE_STRUCT_TESTS(none, WANT_SUCCESS);
DEFINE_TEST(user, struct test_user, STRUCT, none); /* Initialization of members with __user attribute. */
DEFINE_TEST(user, struct test_user, STRUCT, none, WANT_SUCCESS);
/* /*
* Check two uses through a variable declaration outside either path, * Check two uses through a variable declaration outside either path,
@ -285,6 +394,10 @@ DEFINE_TEST(user, struct test_user, STRUCT, none);
static int noinline __leaf_switch_none(int path, bool fill) static int noinline __leaf_switch_none(int path, bool fill)
{ {
switch (path) { switch (path) {
/*
* This is intentionally unreachable. To silence the
* warning, build with -Wno-switch-unreachable
*/
uint64_t var; uint64_t var;
case 1: case 1:
@ -334,8 +447,8 @@ static noinline __init int leaf_switch_2_none(unsigned long sp, bool fill,
* non-code areas (i.e. in a switch statement before the first "case"). * non-code areas (i.e. in a switch statement before the first "case").
* https://bugs.llvm.org/show_bug.cgi?id=44916 * https://bugs.llvm.org/show_bug.cgi?id=44916
*/ */
DEFINE_TEST_DRIVER(switch_1_none, uint64_t, SCALAR, 1); DEFINE_TEST_DRIVER(switch_1_none, uint64_t, SCALAR, XFAIL);
DEFINE_TEST_DRIVER(switch_2_none, uint64_t, SCALAR, 1); DEFINE_TEST_DRIVER(switch_2_none, uint64_t, SCALAR, XFAIL);
static int __init test_stackinit_init(void) static int __init test_stackinit_init(void)
{ {
@ -361,12 +474,18 @@ static int __init test_stackinit_init(void)
test_structs(zero); test_structs(zero);
/* Padding here appears to be accidentally always initialized? */ /* Padding here appears to be accidentally always initialized? */
test_structs(dynamic_partial); test_structs(dynamic_partial);
test_structs(assigned_dynamic_partial);
/* Padding initialization depends on compiler behaviors. */ /* Padding initialization depends on compiler behaviors. */
test_structs(static_partial); test_structs(static_partial);
test_structs(static_all); test_structs(static_all);
test_structs(dynamic_all); test_structs(dynamic_all);
test_structs(runtime_partial); test_structs(runtime_partial);
test_structs(runtime_all); test_structs(runtime_all);
test_structs(assigned_static_partial);
test_structs(assigned_static_all);
test_structs(assigned_dynamic_all);
/* Everything fails this since it effectively performs a memcpy(). */
test_structs(assigned_copy);
/* STRUCTLEAK_BYREF_ALL should cover everything from here down. */ /* STRUCTLEAK_BYREF_ALL should cover everything from here down. */
test_scalars(none); test_scalars(none);

View File

@ -29,6 +29,7 @@ choice
prompt "Initialize kernel stack variables at function entry" prompt "Initialize kernel stack variables at function entry"
default GCC_PLUGIN_STRUCTLEAK_BYREF_ALL if COMPILE_TEST && GCC_PLUGINS default GCC_PLUGIN_STRUCTLEAK_BYREF_ALL if COMPILE_TEST && GCC_PLUGINS
default INIT_STACK_ALL_PATTERN if COMPILE_TEST && CC_HAS_AUTO_VAR_INIT_PATTERN default INIT_STACK_ALL_PATTERN if COMPILE_TEST && CC_HAS_AUTO_VAR_INIT_PATTERN
default INIT_STACK_ALL_ZERO if CC_HAS_AUTO_VAR_INIT_PATTERN
default INIT_STACK_NONE default INIT_STACK_NONE
help help
This option enables initialization of stack variables at This option enables initialization of stack variables at
@ -39,11 +40,11 @@ choice
syscalls. syscalls.
This chooses the level of coverage over classes of potentially This chooses the level of coverage over classes of potentially
uninitialized variables. The selected class will be uninitialized variables. The selected class of variable will be
initialized before use in a function. initialized before use in a function.
config INIT_STACK_NONE config INIT_STACK_NONE
bool "no automatic initialization (weakest)" bool "no automatic stack variable initialization (weakest)"
help help
Disable automatic stack variable initialization. Disable automatic stack variable initialization.
This leaves the kernel vulnerable to the standard This leaves the kernel vulnerable to the standard
@ -80,7 +81,7 @@ choice
and is disallowed. and is disallowed.
config GCC_PLUGIN_STRUCTLEAK_BYREF_ALL config GCC_PLUGIN_STRUCTLEAK_BYREF_ALL
bool "zero-init anything passed by reference (very strong)" bool "zero-init everything passed by reference (very strong)"
depends on GCC_PLUGINS depends on GCC_PLUGINS
depends on !(KASAN && KASAN_STACK) depends on !(KASAN && KASAN_STACK)
select GCC_PLUGIN_STRUCTLEAK select GCC_PLUGIN_STRUCTLEAK
@ -91,33 +92,44 @@ choice
of uninitialized stack variable exploits and information of uninitialized stack variable exploits and information
exposures. exposures.
As a side-effect, this keeps a lot of variables on the
stack that can otherwise be optimized out, so combining
this with CONFIG_KASAN_STACK can lead to a stack overflow
and is disallowed.
config INIT_STACK_ALL_PATTERN config INIT_STACK_ALL_PATTERN
bool "0xAA-init everything on the stack (strongest)" bool "pattern-init everything (strongest)"
depends on CC_HAS_AUTO_VAR_INIT_PATTERN depends on CC_HAS_AUTO_VAR_INIT_PATTERN
help help
Initializes everything on the stack with a 0xAA Initializes everything on the stack (including padding)
pattern. This is intended to eliminate all classes with a specific debug value. This is intended to eliminate
of uninitialized stack variable exploits and information all classes of uninitialized stack variable exploits and
exposures, even variables that were warned to have been information exposures, even variables that were warned about
left uninitialized. having been left uninitialized.
Pattern initialization is known to provoke many existing bugs Pattern initialization is known to provoke many existing bugs
related to uninitialized locals, e.g. pointers receive related to uninitialized locals, e.g. pointers receive
non-NULL values, buffer sizes and indices are very big. non-NULL values, buffer sizes and indices are very big. The
pattern is situation-specific; Clang on 64-bit uses 0xAA
repeating for all types and padding except float and double
which use 0xFF repeating (-NaN). Clang on 32-bit uses 0xFF
repeating for all types and padding.
config INIT_STACK_ALL_ZERO config INIT_STACK_ALL_ZERO
bool "zero-init everything on the stack (strongest and safest)" bool "zero-init everything (strongest and safest)"
depends on CC_HAS_AUTO_VAR_INIT_ZERO depends on CC_HAS_AUTO_VAR_INIT_ZERO
help help
Initializes everything on the stack with a zero Initializes everything on the stack (including padding)
value. This is intended to eliminate all classes with a zero value. This is intended to eliminate all
of uninitialized stack variable exploits and information classes of uninitialized stack variable exploits and
exposures, even variables that were warned to have been information exposures, even variables that were warned
left uninitialized. about having been left uninitialized.
Zero initialization provides safe defaults for strings, Zero initialization provides safe defaults for strings
pointers, indices and sizes, and is therefore (immediately NUL-terminated), pointers (NULL), indices
more suitable as a security mitigation measure. (index 0), and sizes (0 length), so it is therefore more
suitable as a production security mitigation than pattern
initialization.
endchoice endchoice
@ -217,6 +229,25 @@ config INIT_ON_FREE_DEFAULT_ON
touching "cold" memory areas. Most cases see 3-5% impact. Some touching "cold" memory areas. Most cases see 3-5% impact. Some
synthetic workloads have measured as high as 8%. synthetic workloads have measured as high as 8%.
config CC_HAS_ZERO_CALL_USED_REGS
def_bool $(cc-option,-fzero-call-used-regs=used-gpr)
config ZERO_CALL_USED_REGS
bool "Enable register zeroing on function exit"
depends on CC_HAS_ZERO_CALL_USED_REGS
help
At the end of functions, always zero any caller-used register
contents. This helps ensure that temporary values are not
leaked beyond the function boundary. This means that register
contents are less likely to be available for side channels
and information exposures. Additionally, this helps reduce the
number of useful ROP gadgets by about 20% (and removes compiler
generated "write-what-where" gadgets) in the resulting kernel
image. This has a less than 1% performance impact on most
workloads. Image size growth depends on architecture, and should
be evaluated for suitability. For example, x86_64 grows by less
than 1%, and arm64 grows by about 5%.
endmenu endmenu
endmenu endmenu