hardening: statically allocated protected memory
Subsystems (13)
v4 cover hardening: statically allocated protected memory
To: Andy Lutomirski <[email protected]>,
To: Matthew Wilcox <[email protected]>,
To: Nadav Amit <[email protected]>
To: Peter Zijlstra <[email protected]>,
To: Dave Hansen <[email protected]>,
To: Mimi Zohar <[email protected]>
To: Thiago Jung Bauermann <[email protected]>
CC: Kees Cook <[email protected]>
CC: Ahmed Soliman <[email protected]>
CC: linux-integrity <[email protected]>
CC: Kernel Hardening <[email protected]>
CC: Linux-MM <[email protected]>
CC: Linux Kernel Mailing List <[email protected]>
Hello,
at last I'm able to resume work on the memory protection patchset I've proposed some time ago. This version should address comments received so far and introduce support for arm64. Details below.
Patch-set implementing write-rare memory protection for statically allocated data.
Its purpose is to keep write protected the kernel data which is seldom modified, especially if altering it can be exploited during an attack.
There is no read overhead, however writing requires special operations that are probably unsuitable for often-changing data.
The use is opt-in, by applying the modifier __wr_after_init to a variable declaration.
As the name implies, the write protection kicks in only after init() is completed; before that moment, the data is modifiable in the usual way.
Current Limitations:
* supports only data which is allocated statically, at build time.
* supports only x86_64 and arm64;other architectures need to provide own backend
Some notes:
- in case an architecture doesn't support write rare, the behavior is to fallback to regular write operations
- before altering any memory, the destination is sanitized
- write rare data is segregated into own set of pages
- only x86_64 and arm64 supported, atm
- the memset_user() assembly functions seems to work, but I'm not too sure they are really ok
- I've added a simple example: the protection of ima_policy_flags
- the last patch is optional, but it seemed worth to do the refactoring
- the x86_64 user space address range is double the size of the kernel address space, so it's possible to randomize the beginning of the mapping of the kernel address space, but on arm64 they have the same size, so it's not possible to do the same
- I'm not sure if it's correct, since it doesn't seem to be that common in kernel sources, but instead of using #defines for overriding default function calls, I'm using "weak" for the default functions.
- unaddressed: Nadav proposed to do:
#define __wr __attribute__((address_space(5)))
but I don't know exactly where to use it atm
Changelog:
v3->v4
------
* added function for setting memory in user space mapping for arm64
* refactored code, to work with both supported architectures
* reduced dependency on x86_64 specific code, to support by default also arm64
* improved memset_user() for x86_64, but I'm not sure if I understood correctly what was the best way to enhance it.
v2->v3
------
* both wr_memset and wr_memcpy are implemented as generic functions the arch code must provide suitable helpers
* regular initialization for ima_policy_flags: it happens during init
* remove spurious code from the initialization function
v1->v2
------
* introduce cleaner split between generic and arch code
* add x86_64 specific memset_user()
* replace kernel-space memset() memcopy() with userspace counterpart
* randomize the base address for the alternate map across the entire available address range from user space (128TB - 64TB)
* convert BUG() to WARN()
* turn verification of written data into debugging option
* wr_rcu_assign_pointer() as special case of wr_assign()
* example with protection of ima_policy_flags
* documentation
Igor Stoppa (12):
__wr_after_init: Core and default arch __wr_after_init: x86_64: memset_user() __wr_after_init: x86_64: randomize mapping offset __wr_after_init: x86_64: enable __wr_after_init: arm64: memset_user() __wr_after_init: arm64: enable __wr_after_init: Documentation: self-protection __wr_after_init: lkdtm test __wr_after_init: rodata_test: refactor tests __wr_after_init: rodata_test: test __wr_after_init __wr_after_init: test write rare functionality
IMA: turn ima_policy_flags into __wr_after_init
Documentation/security/self-protection.rst | 14 +- arch/Kconfig | 7 + arch/arm64/Kconfig | 1 + arch/arm64/include/asm/uaccess.h | 9 ++ arch/arm64/lib/Makefile | 2 +- arch/arm64/lib/memset_user.S (new) | 63 ++++++++ arch/x86/Kconfig | 1 + arch/x86/include/asm/uaccess_64.h | 6 + arch/x86/lib/usercopy_64.c | 51 ++++++ arch/x86/mm/Makefile | 2 + arch/x86/mm/prmem.c (new) | 20 +++ drivers/misc/lkdtm/core.c | 3 + drivers/misc/lkdtm/lkdtm.h | 3 + drivers/misc/lkdtm/perms.c | 29 ++++ include/linux/prmem.h (new) | 71 ++++++++ mm/Kconfig.debug | 8 + mm/Makefile | 2 + mm/prmem.c (new) | 179 +++++++++++++++++++++ mm/rodata_test.c | 69 +++++--- mm/test_write_rare.c (new) | 136 ++++++++++++++++ security/integrity/ima/ima.h | 3 +- security/integrity/ima/ima_policy.c | 9 +- 22 files changed, 656 insertions(+), 32 deletions(-) create mode 100644 arch/arm64/lib/memset_user.S create mode 100644 arch/x86/mm/prmem.c create mode 100644 include/linux/prmem.h create mode 100644 mm/prmem.c create mode 100644 mm/test_write_rare.c
Signature · 1 line
2.19.1
RFC PATCH v4 1/12 __wr_after_init: Core and default arch
The patch provides:
- the core functionality for write-rare after init for statically allocated data, based on code from Matthew Wilcox
- the default implementation for generic architecture
A specific architecture can override one or more of the default functions.
The core (API) functions are:
- wr_memset(): write rare counterpart of memset()
- wr_memcpy(): write rare counterpart of memcpy()
- wr_assign(): write rare counterpart of the assignment ('=') operator
- wr_rcu_assign_pointer(): write rare counterpart of rcu_assign_pointer()
In case either the selected architecture doesn't support write rare after init, or the functionality is disabled, the write rare functions will resolve into their non-write rare counterpart:
- memset()
- memcpy()
- assignment operator
- rcu_assign_pointer()
For code that can be either link as module or as built-in (ex: device driver init function), it is not possible to tell upfront what will be the case. For this scenario if the functions are called during system init, they will automatically choose, at runtime, to go through the fast path of non-write rare. Should they be invoked later, during module init, they will use the write-rare path.
--- arch/Kconfig | 7 ++ include/linux/prmem.h (new) | 71 +++++++++++++++ mm/Makefile | 1 + mm/prmem.c (new) | 179 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 258 insertions(+)
arch/Kconfig +7 -0
@@ -814,6 +814,13 @@ config ARCH_HAS_PRMEM architecture specific symbol stating that the architecture provides a back-end function for the write rare operation. +config ARCH_HAS_PRMEM_HEADER + def_bool n + depends on ARCH_HAS_PRMEM + help + architecture specific symbol stating that the architecture provides + own specific header back-end for the write rare operation. + config PRMEM bool "Write protect critical data that doesn't need high write speed." depends on ARCH_HAS_PRMEM
include/linux/prmem.h new file +71 -0
71 lines not on this page · raw message
mm/Makefile +1 -0
@@ -58,6 +58,7 @@ obj-$(CONFIG_SPARSEMEM) += sparse.o obj-$(CONFIG_SPARSEMEM_VMEMMAP) += sparse-vmemmap.o obj-$(CONFIG_SLOB) += slob.o obj-$(CONFIG_MMU_NOTIFIER) += mmu_notifier.o +obj-$(CONFIG_PRMEM) += prmem.o obj-$(CONFIG_KSM) += ksm.o obj-$(CONFIG_PAGE_POISONING) += page_poison.o obj-$(CONFIG_SLAB) += slab.o
mm/prmem.c new file +179 -0
179 lines not on this page · raw message
Signature · 1 line
2.19.1
Re: [RFC PATCH v4 01/12] __wr_after_init: Core and default arch
Quoted 1 line · Igor Stoppa
+#ifndef
CONFIG_PRMEM
[...]
include/linux/prmem.h line 32
Quoted 3 lines
include/linux/prmem.h new file +3
-0+#else + +#include <linux/mm.h>
It's a mistake to do conditional includes like this. That way you see include loops with some configs and not others. Our headers are already so messy, better to just include mm.h unconditionally.
Re: [RFC PATCH v4 01/12] __wr_after_init: Core and default arch
Quoted 11 lines · Matthew Wilcox
Quoted 1 line · Igor Stoppa
+#ifndef
CONFIG_PRMEM[...]
include/linux/prmem.h line 32
Quoted 3 lines
include/linux/prmem.h new file +3
-0+#else + +#include <linux/mm.h> It's a mistake to do conditional includes like this. That way you see include loops with some configs and not others. Our headers are already so messy, better to just include
mm.hunconditionally.
ok
Can I still do the following, in prmem.c ?
#ifdef CONFIG_ARCH_HAS_PRMEM_HEADER
+#include <asm/prmem.h>
+#else
+
+struct wr_state {
+ struct mm_struct *prev;
+};
+
+#endif
It's still a conditional include, but it's in a C file, it shouldn't cause any chance of loops.
The alternative is that each arch supporting prmem must have a (probably) empty asm/prmem.h header.
I did some reasearch about telling the compiler to include a header only if it exists, but it doesn't seem to be a gcc feature.
--
igor
RFC PATCH v4 2/12 __wr_after_init: x86_64: memset_user()
In reply to Igor Stoppax86_64 specific version of memset() for user space, memset_user()
In the __wr_after_init scenario, write-rare variables have:
- a primary read-only mapping in kernel memory space
- an alternate, writable mapping, implemented as user-space mapping
The write rare implementation expects the arch code to privide a memset_user() function, which is currently missing.
clear_user() is the base for memset_user()
--- arch/x86/include/asm/uaccess_64.h | 6 ++++ arch/x86/lib/usercopy_64.c | 51 +++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+)
arch/x86/include/asm/uaccess_64.h +6 -0
@@ -213,4 +213,10 @@ copy_user_handle_tail(char *to, char *from, unsigned len);unsigned long mcsafe_handle_tail (char *to,char *from,unsigned len); +unsigned long __must_check +memset_user (void __user *mem,int c,unsigned long len); + +unsigned long __must_check +__memset_user (void __user *mem,int c,unsigned long len); +#endif /* _ASM_X86_UACCESS_64_H */
arch/x86/lib/usercopy_64.c +51 -0
57 lines not on this page · raw message
Signature · 1 line
2.19.1
RFC PATCH v4 3/12 __wr_after_init: x86_64: randomize mapping offset
In reply to Igor Stoppax86_64 specialized way of defining the base address for the alternate mapping used by write-rare.
Since the kernel address space spans across 64TB and it is mapped into a used address space of 128TB, the kernel address space can be shifted by a random offset that is up to 64TB and page aligned.
This is accomplished by providing arch-specific version of the function __init_wr_base()
--- arch/x86/mm/Makefile | 2 ++ arch/x86/mm/prmem.c (new) | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+)
arch/x86/mm/Makefile +2 -0
@@ -53,3 +53,5 @@ obj-$(CONFIG_PAGE_TABLE_ISOLATION) += pti.o obj-$(CONFIG_AMD_MEM_ENCRYPT) += mem_encrypt.o obj-$(CONFIG_AMD_MEM_ENCRYPT) += mem_encrypt_identity.o obj-$(CONFIG_AMD_MEM_ENCRYPT) += mem_encrypt_boot.o + +obj-$(CONFIG_PRMEM) += prmem.o
arch/x86/mm/prmem.c new file +20 -0
@@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-2.0 +/* +* prmem.c: Memory Protection Library - x86_64 backend +* +* (C) Copyright 2018-2019 Huawei Technologies Co. Ltd. +* Author: Igor Stoppa <[email protected]> +*/ + +#include <linux/mm.h> +#include <linux/mmu_context.h> + +unsigned long __init __init_wr_base (void ) +{ +/* +* Place 64TB of kernel address space within 128TB of user address +* space, at a random page aligned offset. +*/ +return (((unsigned long )kaslr_get_random_long ("WR Poke" )) & +PAGE_MASK ) % (64 *_BITUL (40 )); +}
Signature · 1 line
2.19.1
RFC PATCH v4 4/12 __wr_after_init: x86_64: enable
In reply to Igor StoppaSet ARCH_HAS_PRMEM to Y for x86_64
--- arch/x86/Kconfig | 1 + 1 file changed, 1 insertion(+)
arch/x86/Kconfig +1 -0
@@ -32,6 +32,7 @@ config X86_64 select SWIOTLB select X86_DEV_DMA_OPS select ARCH_HAS_SYSCALL_WRAPPER + select ARCH_HAS_PRMEM # # Arch settings
Signature · 1 line
2.19.1
RFC PATCH v4 5/12 __wr_after_init: arm64: memset_user()
In reply to Igor Stoppaarm64 specific version of memset() for user space, memset_user()
In the __wr_after_init scenario, write-rare variables have:
- a primary read-only mapping in kernel memory space
- an alternate, writable mapping, implemented as user-space mapping
The write rare implementation expects the arch code to privide a memset_user() function, which is currently missing.
clear_user() is the base for memset_user()
--- arch/arm64/include/asm/uaccess.h | 9 +++++ arch/arm64/lib/Makefile | 2 +- arch/arm64/lib/memset_user.S (new) | 63 ++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-)
arch/arm64/include/asm/uaccess.h +9 -0
@@ -415,6 +415,15 @@ extern unsigned long __must_check __arch_copy_in_user(void __user *to, const voi#define INLINE_COPY_TO_USER #define INLINE_COPY_FROM_USER +extern unsigned long __must_check __arch_memset_user (void __user *to,int c,unsigned long n); +static inline unsigned long __must_check __memset_user (void __user *to,int c,unsigned long n) +{ +if (access_ok (to, n)) + n =__arch_memset_user (__uaccess_mask_ptr (to), c, n); +return n; +} +#define memset_user __memset_user +extern unsigned long __must_check __arch_clear_user (void __user *to,unsigned long n);static inline unsigned long __must_check __clear_user (void __user *to,unsigned long n) {
arch/arm64/lib/Makefile +1 -1
@@ -1,5 +1,5 @@ # SPDX-License-Identifier: GPL-2.0 -lib-y := clear_user.o delay.o copy_from_user.o \ +lib-y := clear_user.o memset_user.o delay.o copy_from_user.o \ copy_to_user.o copy_in_user.o copy_page.o \ clear_page.o memchr.o memcpy.o memmove.o memset.o \ memcmp.o strcmp.o strncmp.o strlen.o strnlen.o \
arch/arm64/lib/memset_user.S new file +63 -0
63 lines not on this page · raw message
Signature · 1 line
2.19.1
RFC PATCH v4 6/12 __wr_after_init: arm64: enable
In reply to Igor StoppaSet ARCH_HAS_PRMEM to Y for arm64
--- arch/arm64/Kconfig | 1 + 1 file changed, 1 insertion(+)
arch/arm64/Kconfig +1 -0
@@ -66,6 +66,7 @@ config ARM64 select ARCH_WANT_COMPAT_IPC_PARSE_VERSION select ARCH_WANT_FRAME_POINTERS select ARCH_HAS_UBSAN_SANITIZE_ALL + select ARCH_HAS_PRMEM select ARM_AMBA select ARM_ARCH_TIMER select ARM_GIC
Signature · 1 line
2.19.1
RFC PATCH v4 7/12 __wr_after_init: Documentation: self-protection
In reply to Igor StoppaUpdate the self-protection documentation, to mention also the use of the __wr_after_init attribute.
--- Documentation/security/self-protection.rst | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-)
Documentation/security/self-protection.rst +8 -6
@@ -84,12 +84,14 @@ For variables that are initialized once at ``__init`` time, these can be marked with the (new and under development) ``__ro_after_init`` attribute. -What remains are variables that are updated rarely (e.g. GDT). These -will need another infrastructure (similar to the temporary exceptions -made to kernel code mentioned above) that allow them to spend the rest -of their lifetime read-only. (For example, when being updated, only the -CPU thread performing the update would be given uninterruptible write -access to the memory.) +Others, which are statically allocated, but still need to be updated +rarely, can be marked with the ``__wr_after_init`` attribute. + +The update mechanism must avoid exposing the data to rogue alterations +during the update. For example, only the CPU thread performing the update +would be given uninterruptible write access to the memory. + +Currently there is no protection available for data allocated dynamically. Segregation of kernel memory from userspace memory ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Signature · 1 line
2.19.1
RFC PATCH v4 8/12 __wr_after_init: lkdtm test
In reply to Igor StoppaVerify that trying to modify a variable with the __wr_after_init attribute will cause a crash.
--- drivers/misc/lkdtm/core.c | 3 +++ drivers/misc/lkdtm/lkdtm.h | 3 +++ drivers/misc/lkdtm/perms.c | 29 +++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+)
drivers/misc/lkdtm/core.c +3 -0
@@ -155,6 +155,9 @@ static const struct crashtype crashtypes[] = {CRASHTYPE (ACCESS_USERSPACE ),CRASHTYPE (WRITE_RO ),CRASHTYPE (WRITE_RO_AFTER_INIT ), +#ifdef CONFIG_PRMEM +CRASHTYPE (WRITE_WR_AFTER_INIT ), +#endifCRASHTYPE (WRITE_KERN ),CRASHTYPE (REFCOUNT_INC_OVERFLOW ), CRASHTYPE(REFCOUNT_ADD_OVERFLOW),
drivers/misc/lkdtm/lkdtm.h +3 -0
@@ -38,6 +38,9 @@ void lkdtm_READ_BUDDY_AFTER_FREE(void);void __init lkdtm_perms_init (void );void lkdtm_WRITE_RO (void );void lkdtm_WRITE_RO_AFTER_INIT (void ); +#ifdef CONFIG_PRMEM +void lkdtm_WRITE_WR_AFTER_INIT (void ); +#endif void lkdtm_WRITE_KERN (void );void lkdtm_EXEC_DATA (void );void lkdtm_EXEC_STACK (void );
drivers/misc/lkdtm/perms.c +29 -0
@@ -9,6 +9,7 @@#include <linux/vmalloc.h> #include <linux/mman.h> #include <linux/uaccess.h> +#include <linux/prmem.h> #include <asm/cacheflush.h> /* Whether or not to fill the target memory area with do_nothing(). */
@@ -27,6 +28,10 @@ static const unsigned long rodata = 0xAA55AA55;/* This is marked __ro_after_init, so it should ultimately be .rodata. */ static unsigned long ro_after_init__ro_after_init =0x55AA5500 ; +/* This is marked __wr_after_init, so it should be in .rodata. */ +static +unsigned long wr_after_init __wr_after_init =0x55AA5500 ; +/* * This just returns to the caller. It is designed to be copied into * non-executable memory regions.
@@ -104,6 +109,28 @@ void lkdtm_WRITE_RO_AFTER_INIT(void) *ptr ^=0xabcd1234 ; } +#ifdef CONFIG_PRMEM + +void lkdtm_WRITE_WR_AFTER_INIT (void ) +{ +unsigned long *ptr = &wr_after_init; + +/* +* Verify we were written to during init. Since an Oops +* is considered a "success", a failure is to just skip the +* real test. +*/ +if ((*ptr &0xAA ) !=0xAA ) { +pr_info ("%p was NOT written during init!?\n" , ptr); +return ; + } + +pr_info ("attempting bad wr_after_init write at %p\n" , ptr); + *ptr ^=0xabcd1234 ; +} + +#endif +void lkdtm_WRITE_KERN (void ) {size_t size;
@@ -200,4 +227,6 @@ void __init lkdtm_perms_init(void)/* Make sure we can write to __ro_after_init values during __init */ ro_after_init |=0xAA ; +/* Make sure we can write to __wr_after_init during __init */ + wr_after_init |=0xAA ; }
Signature · 1 line
2.19.1
RFC PATCH v4 9/12 __wr_after_init: rodata_test: refactor tests
In reply to Igor StoppaRefactor the test cases, in preparation for using them also for testing __wr_after_init memory, when available.
--- mm/rodata_test.c | 48 ++++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-)
mm/rodata_test.c +28 -20
@@ -14,44 +14,52 @@#include <linux/uaccess.h> #include <asm/sections.h> -static const int rodata_test_data =0xC3 ; +#define INIT_TEST_VAL 0xC3 -void rodata_test (void ) +static const int rodata_test_data =INIT_TEST_VAL ; + +static bool test_data (char *data_type,const int *data, +unsigned long start,unsigned long end) { -unsigned long start, end;int zero =0 ;/* test 1: read the value */ /* If this test fails, some previous testrun has clobbered the state */ -if (!rodata_test_data) { -pr_err ("test 1 fails (start data)\n" ); -return ; +if (*data !=INIT_TEST_VAL ) { +pr_err ("%s: test 1 fails (init data value)\n" , data_type); +return false; }/* test 2: write to the variable; this should fault */ -if (!probe_kernel_write ((void *)&rodata_test_data, - (void *)&zero,sizeof (zero))) { -pr_err ("test data was not read only\n" ); -return ; +if (!probe_kernel_write ((void *)data, (void *)&zero,sizeof (zero))) { +pr_err ("%s: test data was not read only\n" , data_type); +return false; }/* test 3: check the value hasn't changed */ -if (rodata_test_data == zero) { -pr_err ("test data was changed\n" ); -return ; +if (*data !=INIT_TEST_VAL ) { +pr_err ("%s: test data was changed\n" , data_type); +return false; }/* test 4: check if the rodata section is PAGE_SIZE aligned */ - start = (unsigned long )__start_rodata; - end = (unsigned long )__end_rodata;if (start & (PAGE_SIZE -1 )) { -pr_err ("start of .rodata is not page size aligned\n" ); -return ; +pr_err ("%s: start of data is not page size aligned\n" , + data_type); +return false; }if (end & (PAGE_SIZE -1 )) { -pr_err ("end of .rodata is not page size aligned\n" ); -return ; +pr_err ("%s: end of data is not page size aligned\n" , + data_type); +return false; } +pr_info ("%s tests were successful" , data_type); +return true; +} -pr_info ("all tests were successful\n" ); +void rodata_test (void ) +{ +test_data ("rodata" , &rodata_test_data, + (unsigned long )&__start_rodata, + (unsigned long )&__end_rodata); }
Signature · 1 line
2.19.1
RFC PATCH v4 10/12 __wr_after_init: rodata_test: test __wr_after_init
In reply to Igor StoppaThe write protection of the __wr_after_init data can be verified with the same methodology used for const data.
--- mm/rodata_test.c | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-)
mm/rodata_test.c +24 -3
@@ -16,8 +16,23 @@#define INIT_TEST_VAL 0xC3 +/* +* Note: __ro_after_init data is, for every practical effect, equivalent to +* const data, since they are even write protected at the same time; there +* is no need for separate testing. +* __wr_after_init data, otoh, is altered also after the write protection +* takes place and it cannot be exploitable for altering more permanent +* data. +*/ +static const int rodata_test_data =INIT_TEST_VAL ; +#ifdef CONFIG_PRMEM +static int wr_after_init_test_data __wr_after_init = INIT_TEST_VAL; +extern long __start_wr_after_init; +extern long __end_wr_after_init; +#endif +static bool test_data (char *data_type,const int *data,unsigned long start,unsigned long end) {
@@ -59,7 +74,13 @@ static bool test_data(char *data_type, const int *data,void rodata_test (void ) { -test_data ("rodata" , &rodata_test_data, - (unsigned long )&__start_rodata, - (unsigned long )&__end_rodata); +if (!test_data ("rodata" , &rodata_test_data, + (unsigned long )&__start_rodata, + (unsigned long )&__end_rodata)) +return ; +#ifdef CONFIG_PRMEM +test_data ("wr after init data" , &wr_after_init_test_data, + (unsigned long )&__start_wr_after_init, + (unsigned long )&__end_wr_after_init); +#endif }
Signature · 1 line
2.19.1
RFC PATCH v4 11/12 __wr_after_init: test write rare functionality
In reply to Igor StoppaSet of test cases meant to confirm that the write rare functionality works as expected.
It can be optionally compiled as module.
--- mm/Kconfig.debug | 8 +++ mm/Makefile | 1 + mm/test_write_rare.c (new) | 136 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+)
mm/Kconfig.debug +8 -0
@@ -94,3 +94,11 @@ config DEBUG_RODATA_TEST depends on STRICT_KERNEL_RWX ---help--- This option enables a testcase for the setting rodata read-only. + +config DEBUG_PRMEM_TEST + tristate "Run self test for statically allocated protected memory" + depends on PRMEM + default n + help + Tries to verify that the protection for statically allocated memory + works correctly and that the memory is effectively protected.
mm/Makefile +1 -0
@@ -59,6 +59,7 @@ obj-$(CONFIG_SPARSEMEM_VMEMMAP) += sparse-vmemmap.o obj-$(CONFIG_SLOB) += slob.o obj-$(CONFIG_MMU_NOTIFIER) += mmu_notifier.o obj-$(CONFIG_PRMEM) += prmem.o +obj-$(CONFIG_DEBUG_PRMEM_TEST) += test_write_rare.o obj-$(CONFIG_KSM) += ksm.o obj-$(CONFIG_PAGE_POISONING) += page_poison.o obj-$(CONFIG_SLAB) += slab.o
mm/test_write_rare.c new file +136 -0
136 lines not on this page · raw message
Signature · 1 line
2.19.1
RFC PATCH v4 12/12 IMA: turn ima_policy_flags into __wr_after_init
In reply to Igor StoppaThe policy flags could be targeted by an attacker aiming at disabling IMA, so that there would be no trace of a file system modification in the measurement list.
Since the flags can be altered at runtime, it is not possible to make them become fully read-only, for example with __ro_after_init.
__wr_after_init can still provide some protection, at least against simple memory overwrite attacks
--- security/integrity/ima/ima.h | 3 ++- security/integrity/ima/ima_policy.c | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-)
security/integrity/ima/ima.h +2 -1
@@ -24,6 +24,7 @@#include <linux/hash.h> #include <linux/tpm.h> #include <linux/audit.h> +#include <linux/prmem.h> #include <crypto/hash_info.h> #include "../integrity.h"
@@ -50,7 +51,7 @@ enum tpm_pcrs { TPM_PCR0 = 0, TPM_PCR8 = 8 };#define IMA_TEMPLATE_IMA_FMT "d|n"/* current content of the policy */ -extern int ima_policy_flag; +extern int ima_policy_flag __wr_after_init;/* set during initialization */ extern int ima_hash_algo;
security/integrity/ima/ima_policy.c +5 -4
@@ -48,7 +48,7 @@#define INVALID_PCR (a) (((a) < 0) || \ (a) >= (FIELD_SIZEOF(struct integrity_iint_cache, measured_pcrs) * 8)) -int ima_policy_flag; +int ima_policy_flag __wr_after_init;static int temp_ima_appraise;static int build_ima_appraise__ro_after_init ;
@@ -460,12 +460,13 @@ void ima_update_policy_flag(void)list_for_each_entry (entry, ima_rules, list) {if (entry->action &IMA_DO_MASK ) - ima_policy_flag |= entry->action; +wr_assign (ima_policy_flag, + ima_policy_flag | entry->action); } ima_appraise |= (build_ima_appraise | temp_ima_appraise);if (!ima_appraise) - ima_policy_flag &= ~IMA_APPRAISE ; +wr_assign (ima_policy_flag, ima_policy_flag & ~IMA_APPRAISE ); }static int ima_appraise_flag (enum ima_hooks func)
@@ -651,7 +652,7 @@ void ima_update_policy(void)list_splice_tail_init_rcu (&ima_temp_rules, policy, synchronize_rcu);if (ima_rules != policy) { - ima_policy_flag =0 ; +wr_assign (ima_policy_flag,0 ); ima_rules = policy;/*
Signature · 1 line
2.19.1
Quoted 3 lines · Igor Stoppa
at last I'm able to resume work on the memory protection patchset I've proposed some time ago. This version should address comments received so far and introduce support for arm64. Details below.
Cool!
Quoted 2 lines
Patch-set implementing write-rare memory protection for statically allocated data.
It seems like this could be expanded in the future to cover dynamic memory too (i.e. just a separate base range in the mm).
Quoted 8 lines
Its purpose is to keep write protected the kernel data which is seldom modified, especially if altering it can be exploited during an attack.
There is no read overhead, however writing requires special operations that are probably unsuitable for often-changing data.
The use is opt-in, by applying the modifier__wr_after_initto a variable declaration.
Quoted 7 lines
As the name implies, the write protection kicks in only after
init()is completed; before that moment, the data is modifiable in the usual way.Current Limitations:
* supports only data which is allocated statically, at build time.
* supports onlyx86_64and arm64;other architectures need to provide own backend
It looked like only the memset() needed architecture support. Is there a reason for not being able to implement memset() in terms of an inefficient put_user() loop instead? That would eliminate the need for per-arch support, yes?
Quoted 1 line
- I've added a simple example: the protection of
ima_policy_flags
You'd also looked at SELinux too, yes? What other things could be targeted for protection? (It seems we can't yet protect page tables themselves with this...)
Quoted 4 lines
- the
x86_64user space address range is double the size of the kernel address space, so it's possible to randomize the beginning of the mapping of the kernel address space, but on arm64 they have the same size, so it's not possible to do the same
Only the wr_rare section needs mapping, though, yes?
Quoted 3 lines
- I'm not sure if it's correct, since it doesn't seem to be that common in kernel sources, but instead of using #defines for overriding default function calls, I'm using "weak" for the default functions.
The tradition is to use #defines for easier readability, but "weak" continues to be a thing. *shrug*
This will be a nice addition to protect more of the kernel's static data from write-what-where attacks. :)
Signature · 1 line
Kees Cook
Quoted 1 line · Kees Cook
On Mon, Feb 11, 2019 at 3:28 PM Igor Stoppa <[email protected]> wrote:
[...]
Quoted 5 lines
Quoted 2 lines
Patch-set implementing write-rare memory protection for statically allocated data.
It seems like this could be expanded in the future to cover dynamic memory too (i.e. just a separate base range in the mm).
Indeed. And part of the code refactoring is also geared in that direction. I am working on that part, but it was agreed that I would first provide this subset of features covering statically allocated memory. So I'm sticking to the plan. But this is roughly 1/3 of the basic infra I have in mind.
Quoted 16 lines
Quoted 8 lines
Its purpose is to keep write protected the kernel data which is seldom modified, especially if altering it can be exploited during an attack.
There is no read overhead, however writing requires special operations that are probably unsuitable for often-changing data.
The use is opt-in, by applying the modifier__wr_after_initto a variable declaration.Quoted 7 lines
As the name implies, the write protection kicks in only after
init()is completed; before that moment, the data is modifiable in the usual way.Current Limitations:
* supports only data which is allocated statically, at build time.
* supports onlyx86_64and arm64;other architectures need to provide own backend
Quoted 4 lines
It looked like only the
memset()needed architecture support. Is there a reason for not being able to implementmemset()in terms of an inefficientput_user()loop instead? That would eliminate the need for per-arch support, yes?
So far, yes, however from previous discussion about power arch, I understood this implementation would not be so easy to adapt. Lacking other examples where the extra mapping could be used, I did not want to add code without a use case.
Probably both arm and x86 32 bit could do, but I would like to first get to the bitter end with memory protection (the other 2 thirds).
Mostly, I hated having just one arch and I also really wanted to have arm64.
But eventually, yes, a generic put_user() loop could do, provided that there are other arch where the extra mapping to user space would be a good way to limit write access. This last part is what I'm not sure of.
Quoted 5 lines
Quoted 1 line
- I've added a simple example: the protection of
ima_policy_flagsYou'd also looked at SELinux too, yes? What other things could be targeted for protection? (It seems we can't yet protect page tables themselves with this...)
Yes, I have. See the "1/3" explanation above. I'm also trying to get away with as small example as possible, to get the basic infra merged.
SELinux is not going to be a small patch set. I'd rather move to it once at least some of the framework is merged. It might be a good use case for dynamic allocation, if I do not find something smaller. But for static write rare, going after IMA was easier, and it is still a good target for protection, imho, as flipping this variable should be sufficient for turning IMA off.
For the page tables, I have in mind a little bit different approach, that I hope to explain better once I get to do the dynamic allocation.
Quoted 6 lines
Quoted 4 lines
- the
x86_64user space address range is double the size of the kernel address space, so it's possible to randomize the beginning of the mapping of the kernel address space, but on arm64 they have the same size, so it's not possible to do the sameOnly the
wr_raresection needs mapping, though, yes?
Yup, however, once more, I'm not so keen to do what seems as premature optimization, before I have addressed the framework in its entirety, as the dynamic allocation will need similar treatment.
Quoted 6 lines
Quoted 3 lines
- I'm not sure if it's correct, since it doesn't seem to be that common in kernel sources, but instead of using #defines for overriding default function calls, I'm using "weak" for the default functions.
The tradition is to use #defines for easier readability, but "weak" continues to be a thing. *shrug*
Yes, I wasn't so sure about it, but I kinda liked the fact that, by using "weak", the arch header becomes optional, unless one has to redefine the struct wr_state.
Quoted 2 lines
This will be a nice addition to protect more of the kernel's static data from write-what-where attacks. :)
I hope so :-)
--
thanks, igor
Quoted 15 lines · Igor Stoppa
Quoted 5 lines · Kees Cook
On Mon, Feb 11, 2019 at 3:28 PM Igor Stoppa <[email protected]> wrote:
It looked like only thememset()needed architecture support. Is there a reason for not being able to implementmemset()in terms of an inefficientput_user()loop instead? That would eliminate the need for per-arch support, yes?So far, yes, however from previous discussion about power arch, I understood this implementation would not be so easy to adapt. Lacking other examples where the extra mapping could be used, I did not want to add code without a use case.
Quoted 4 lines
Probably both arm and x86 32 bit could do, but I would like to first get to the bitter end with memory protection (the other 2 thirds).
Mostly, I hated having just one arch and I also really wanted to have arm64.
Right, I meant, if you implemented the _memset() case with put_user() in this version, you could drop the arch-specific _memset() and shrink the patch series. Then you could also enable this across all the architectures in one patch. (Would you even need the Kconfig patches, i.e. won't this "Just Work" on everything with an MMU?)
Signature · 1 line
Kees Cook
Quoted 20 lines · On Tue, 12 Feb 2019, 4.47 Kees Cook <[email protected] wrote:
Quoted 5 lines · Igor Stoppa
Quoted 1 line · Kees Cook
On Mon, Feb 11, 2019 at 3:28 PM Igor Stoppa <[email protected]>
Quoted 13 lines · wrote:
Quoted 4 lines
It looked like only the
memset()needed architecture support. Is there a reason for not being able to implementmemset()in terms of an inefficientput_user()loop instead? That would eliminate the need for per-arch support, yes?So far, yes, however from previous discussion about power arch, I understood this implementation would not be so easy to adapt. Lacking other examples where the extra mapping could be used, I did not want to add code without a use case.
Probably both arm and x86 32 bit could do, but I would like to first get to the bitter end with memory protection (the other 2 thirds).
Quoted 9 lines
Quoted 1 line
Mostly, I hated having just one arch and I also really wanted to have
arm64.
Right, I meant, if you implemented the
_memset()case withput_user()in this version, you could drop the arch-specific_memset()and shrink the patch series. Then you could also enable this across all the architectures in one patch. (Would you even need theKconfigpatches, i.e. won't this "Just Work" on everything with an MMU?)
I had similar thoughts, but this answer [1] deflated my hopes (if I understood it correctly).
It seems that each arch needs to be massaged in separately.
--
igor
[1] https://www.openwall.com/lists/kernel-hardening/2018/12/12/15
Quoted 1 line
Quoted 33 lines · On Mon, Feb 11, 2019 at 5:08 PM [email protected] <[email protected]>…
Quoted 20 lines · On Tue, 12 Feb 2019, 4.47 Kees Cook <[email protected] wrote:
Quoted 18 lines · Igor Stoppa
Quoted 5 lines · Kees Cook
On Mon, Feb 11, 2019 at 3:28 PM Igor Stoppa <[email protected]> wrote:
It looked like only thememset()needed architecture support. Is there a reason for not being able to implementmemset()in terms of an inefficientput_user()loop instead? That would eliminate the need for per-arch support, yes?So far, yes, however from previous discussion about power arch, I understood this implementation would not be so easy to adapt. Lacking other examples where the extra mapping could be used, I did not want to add code without a use case.
Probably both arm and x86 32 bit could do, but I would like to first get to the bitter end with memory protection (the other 2 thirds).
Quoted 7 lines
Quoted 1 line
Mostly, I hated having just one arch and I also really wanted to have arm64.
Right, I meant, if you implemented the
_memset()case withput_user()in this version, you could drop the arch-specific_memset()and shrink the patch series. Then you could also enable this across all the architectures in one patch. (Would you even need theKconfigpatches, i.e. won't this "Just Work" on everything with an MMU?)
Quoted 2 lines
I had similar thoughts, but this answer [1] deflated my hopes (if I understood it correctly). It seems that each arch needs to be massaged in separately.
True, but I think x86_64, x86, arm64, and arm will all be "normal". power may be that way too, but they always surprise me. :)
Anyway, series looks good, but since nothing uses _memset(), it might make sense to leave it out and put all the arch-enabling into a single patch to cover the 4 archs above, in an effort to make the series even smaller.
Signature · 1 line
Kees Cook
Quoted 38 lines · Kees Cook
Quoted 33 lines · On Mon, Feb 11, 2019 at 5:08 PM [email protected] <[email protected]>…
Quoted 20 lines · On Tue, 12 Feb 2019, 4.47 Kees Cook <[email protected] wrote:
Quoted 18 lines · Igor Stoppa
Quoted 5 lines · Kees Cook
On Mon, Feb 11, 2019 at 3:28 PM Igor Stoppa <[email protected]> wrote:
It looked like only thememset()needed architecture support. Is there a reason for not being able to implementmemset()in terms of an inefficientput_user()loop instead? That would eliminate the need for per-arch support, yes?So far, yes, however from previous discussion about power arch, I understood this implementation would not be so easy to adapt. Lacking other examples where the extra mapping could be used, I did not want to add code without a use case.
Probably both arm and x86 32 bit could do, but I would like to first get to the bitter end with memory protection (the other 2 thirds).
Quoted 7 lines
Quoted 1 line
Mostly, I hated having just one arch and I also really wanted to have arm64.
Right, I meant, if you implemented the
_memset()case withput_user()in this version, you could drop the arch-specific_memset()and shrink the patch series. Then you could also enable this across all the architectures in one patch. (Would you even need theKconfigpatches, i.e. won't this "Just Work" on everything with an MMU?)Quoted 2 lines
I had similar thoughts, but this answer [1] deflated my hopes (if I understood it correctly). It seems that each arch needs to be massaged in separately.
Quoted 7 lines
True, but I think
x86_64, x86, arm64, and arm will all be "normal". power may be that way too, but they always surprise me. :)Anyway, series looks good, but since nothing uses
_memset(), it might make sense to leave it out and put all the arch-enabling into a single patch to cover the 4 archs above, in an effort to make the series even smaller.
Actually, I do use it, albeit indirectly.
That's the whole point of having the IMA patch as example.
This is the fragment:
----------------
@@ -460,12 +460,13 @@ void ima_update_policy_flag(void)
list_for_each_entry(entry, ima_rules, list) {
if (entry->action & IMA_DO_MASK)
- ima_policy_flag |= entry->action;
+ wr_assign(ima_policy_flag,
+ ima_policy_flag | entry->action);
}
ima_appraise |= (build_ima_appraise | temp_ima_appraise); if (!ima_appraise)
- ima_policy_flag &= ~IMA_APPRAISE;
+ wr_assign(ima_policy_flag, ima_policy_flag & ~IMA_APPRAISE);
}
----------------
wr_assign() does just that.
However, reading again your previous mails, I realize that I might have misinterpreted what you were suggesting.
If the advice is to have also a default memset_user() which relies on put_user(), but do not activate the feature by default for every architecture, I definitely agree that it would be good to have it. I just didn't think about it before.
What I cannot do is to turn it on for all the architectures prior to test it and atm I do not have means to do it.
But I now realize that most likely you were just suggesting to have full, albeit inefficient default support and then let various archs review/enhance it. I can certainly do this.
Regarding testing I have a question: how much can/should I lean on qemu?
In most cases the MMU might not need to be fully emulated, so I wonder how well qemu-based testing can ensure that real life scenarios will work.
--
igor
Quoted 9 lines · Igor Stoppa
wr_assign()does just that.However, reading again your previous mails, I realize that I might have misinterpreted what you were suggesting.
If the advice is to have also a default
memset_user()which relies onput_user(), but do not activate the feature by default for every architecture, I definitely agree that it would be good to have it. I just didn't think about it before.
Yeah, I just mean you could have an arch-agnostic memset_user() implementation.
Quoted 3 lines
But I now realize that most likely you were just suggesting to have full, albeit inefficient default support and then let various archs review/enhance it. I can certainly do this.
Right.
Quoted 3 lines
Regarding testing I have a question: how much can/should I lean on qemu? In most cases the MMU might not need to be fully emulated, so I wonder how well qemu-based testing can ensure that real life scenarios will work.
I think qemu lets you know if it works (kvm is using the real MMU), and baremetal will give you more stable performance numbers.
Signature · 1 line
Kees Cook