From: Christopher M. Riedl <hidden> Date: 2020-06-03 05:28:41
When compiled with CONFIG_STRICT_KERNEL_RWX, the kernel must create
temporary mappings when patching itself. These mappings temporarily
override the strict RWX text protections to permit a write. Currently,
powerpc allocates a per-CPU VM area for patching. Patching occurs as
follows:
1. Map page of text to be patched to per-CPU VM area w/
PAGE_KERNEL protection
2. Patch text
3. Remove the temporary mapping
While the VM area is per-CPU, the mapping is actually inserted into the
kernel page tables. Presumably, this could allow another CPU to access
the normally write-protected text - either malicously or accidentally -
via this same mapping if the address of the VM area is known. Ideally,
the mapping should be kept local to the CPU doing the patching (or any
other sensitive operations requiring temporarily overriding memory
protections) [0].
x86 introduced "temporary mm" structs which allow the creation of
mappings local to a particular CPU [1]. This series intends to bring the
notion of a temporary mm to powerpc and harden powerpc by using such a
mapping for patching a kernel with strict RWX permissions.
The first patch introduces the temporary mm struct and API for powerpc
along with a new function to retrieve a current hw breakpoint.
The second patch uses the `poking_init` init hook added by the x86
patches to initialize a temporary mm and patching address. The patching
address is randomized between 0 and DEFAULT_MAP_WINDOW-PAGE_SIZE. The
upper limit is necessary due to how the hash MMU operates - by default
the space above DEFAULT_MAP_WINDOW is not available. For now, both hash
and radix randomize inside this range. The number of possible random
addresses is dependent on PAGE_SIZE and limited by DEFAULT_MAP_WINDOW.
Bits of entropy with 64K page size on BOOK3S_64:
bits of entropy = log2(DEFAULT_MAP_WINDOW_USER64 / PAGE_SIZE)
PAGE_SIZE=64K, DEFAULT_MAP_WINDOW_USER64=128TB
bits of entropy = log2(128TB / 64K)
bits of entropy = 31
Randomization occurs only once during initialization at boot.
The third patch replaces the VM area with the temporary mm in the
patching code. The page for patching has to be mapped PAGE_SHARED with
the hash MMU since hash prevents the kernel from accessing userspace
pages with PAGE_PRIVILEGED bit set. On the radix MMU the page is mapped with
PAGE_KERNEL which has the added benefit that we can skip KUAP.
The fourth and fifth patches implement an LKDTM test "proof-of-concept" which
exploits the previous vulnerability (ie. the mapping during patching is exposed
in kernel page tables and accessible by other CPUS). The LKDTM test is somewhat
"rough" in that it uses a brute-force approach - I am open to any suggestions
and/or ideas to improve this. Currently, the LKDTM test passes with this series
on POWER8 (hash) and POWER9 (radix, hash) and fails without this series (ie.
the temporary mapping for patching is exposed to CPUs other than the patching
CPU).
The test can be applied to a tree without this new series by first
adding this in /arch/powerpc/lib/code-patching.c:
#ifdef CONFIG_STRICT_KERNEL_RWX
static DEFINE_PER_CPU(struct vm_struct *, text_poke_area);
+#ifdef CONFIG_LKDTM
+unsigned long read_cpu_patching_addr(unsigned int cpu)
+{
+ return (unsigned long)(per_cpu(text_poke_area, cpu))->addr;
+}
+#endif
+
static int text_area_cpu_up(unsigned int cpu)
{
struct vm_struct *area;
And then applying the last patch of this series which adds the LKDTM test,
(powerpc: Add LKDTM test to hijack a patch mapping).
Tested on QEMU (POWER8, POWER9), POWER8 VM, and a Blackbird (8-core POWER9).
v1: * Rebase on linuxppc/next (4336b9337824)
* Save and restore second hw watchpoint
* Use new ppc_inst_* functions for patching check and in LKDTM
test
rfc-v2: * Many fixes and improvements mostly based on extensive feedback and
testing by Christophe Leroy (thanks!).
* Make patching_mm and patching_addr static and mode '__ro_after_init'
to after the variable name (more common in other parts of the kernel)
* Use 'asm/debug.h' header instead of 'asm/hw_breakpoint.h' to fix
PPC64e compile
* Add comment explaining why we use BUG_ON() during the init call to
setup for patching later
* Move ptep into patch_mapping to avoid walking page tables a second
time when unmapping the temporary mapping
* Use KUAP under non-radix, also manually dirty the PTE for patch
mapping on non-BOOK3S_64 platforms
* Properly return any error from __patch_instruction
* Do not use 'memcmp' where a simple comparison is appropriate
* Simplify expression for patch address by removing pointer maths
* Add LKDTM test
[0]: https://github.com/linuxppc/issues/issues/224
[1]: https://lore.kernel.org/kernel-hardening/20190426232303.28381-1-nadav.amit@gmail.com/
Christopher M. Riedl (5):
powerpc/mm: Introduce temporary mm
powerpc/lib: Initialize a temporary mm for code patching
powerpc/lib: Use a temporary mm for code patching
powerpc/lib: Add LKDTM accessor for patching addr
powerpc: Add LKDTM test to hijack a patch mapping
arch/powerpc/include/asm/debug.h | 1 +
arch/powerpc/include/asm/mmu_context.h | 64 +++++++++
arch/powerpc/kernel/process.c | 5 +
arch/powerpc/lib/code-patching.c | 172 +++++++++++++------------
drivers/misc/lkdtm/core.c | 1 +
drivers/misc/lkdtm/lkdtm.h | 1 +
drivers/misc/lkdtm/perms.c | 101 +++++++++++++++
7 files changed, 260 insertions(+), 85 deletions(-)
--
2.26.2
From: Christopher M. Riedl <hidden> Date: 2020-06-03 05:19:47
x86 supports the notion of a temporary mm which restricts access to
temporary PTEs to a single CPU. A temporary mm is useful for situations
where a CPU needs to perform sensitive operations (such as patching a
STRICT_KERNEL_RWX kernel) requiring temporary mappings without exposing
said mappings to other CPUs. A side benefit is that other CPU TLBs do
not need to be flushed when the temporary mm is torn down.
Mappings in the temporary mm can be set in the userspace portion of the
address-space.
Interrupts must be disabled while the temporary mm is in use. HW
breakpoints, which may have been set by userspace as watchpoints on
addresses now within the temporary mm, are saved and disabled when
loading the temporary mm. The HW breakpoints are restored when unloading
the temporary mm. All HW breakpoints are indiscriminately disabled while
the temporary mm is in use.
Based on x86 implementation:
commit cefa929c034e
("x86/mm: Introduce temporary mm structs")
Signed-off-by: Christopher M. Riedl <redacted>
---
arch/powerpc/include/asm/debug.h | 1 +
arch/powerpc/include/asm/mmu_context.h | 64 ++++++++++++++++++++++++++
arch/powerpc/kernel/process.c | 5 ++
3 files changed, 70 insertions(+)
From: Christopher M. Riedl <hidden> Date: 2020-06-03 05:21:40
When live patching a STRICT_RWX kernel, a mapping is installed at a
"patching address" with temporary write permissions. Provide a
LKDTM-only accessor function for this address in preparation for a LKDTM
test which attempts to "hijack" this mapping by writing to it from
another CPU.
Signed-off-by: Christopher M. Riedl <redacted>
---
arch/powerpc/lib/code-patching.c | 7 +++++++
1 file changed, 7 insertions(+)
From: Christopher M. Riedl <hidden> Date: 2020-06-03 05:23:13
Currently, code patching a STRICT_KERNEL_RWX exposes the temporary
mappings to other CPUs. These mappings should be kept local to the CPU
doing the patching. Use the pre-initialized temporary mm and patching
address for this purpose. Also add a check after patching to ensure the
patch succeeded.
Use the KUAP functions on non-BOOKS3_64 platforms since the temporary
mapping for patching uses a userspace address (to keep the mapping
local). On BOOKS3_64 platforms hash does not implement KUAP and on radix
the use of PAGE_KERNEL sets EAA[0] for the PTE which means the AMR
(KUAP) protection is ignored (see PowerISA v3.0b, Fig, 35).
Based on x86 implementation:
commit b3fd8e83ada0
("x86/alternatives: Use temporary mm for text poking")
Signed-off-by: Christopher M. Riedl <redacted>
---
arch/powerpc/lib/code-patching.c | 148 ++++++++++++-------------------
1 file changed, 55 insertions(+), 93 deletions(-)
@@ -78,101 +79,58 @@ void __init poking_init(void)pte_unmap_unlock(ptep,ptl);}-staticDEFINE_PER_CPU(structvm_struct*,text_poke_area);--staticinttext_area_cpu_up(unsignedintcpu)-{-structvm_struct*area;--area=get_vm_area(PAGE_SIZE,VM_ALLOC);-if(!area){-WARN_ONCE(1,"Failed to create text area for cpu %d\n",-cpu);-return-1;-}-this_cpu_write(text_poke_area,area);--return0;-}--staticinttext_area_cpu_down(unsignedintcpu)-{-free_vm_area(this_cpu_read(text_poke_area));-return0;-}--/*-*Runasalateinitcall.Thisallowsalltheboottimepatchingtobedone-*simplybypatchingthecode,andthenwe'recalledherepriorto-*mark_rodata_ro(),whichhappensafterallinitcallsarerun.Although-*BUG_ON()isrude,inthiscaseitshouldonlyhappenifENOMEM,andwejudge-*itasbeingpreferabletoakernelthatwillcrashlaterwhensomeonetries-*tousepatch_instruction().-*/-staticint__initsetup_text_poke_area(void)-{-BUG_ON(!cpuhp_setup_state(CPUHP_AP_ONLINE_DYN,-"powerpc/text_poke:online",text_area_cpu_up,-text_area_cpu_down));--return0;-}-late_initcall(setup_text_poke_area);+structpatch_mapping{+spinlock_t*ptl;/* for protecting pte table */+pte_t*ptep;+structtemp_mmtemp_mm;+};/**Thiscanbecalledforkerneltextoramodule.*/-staticintmap_patch_area(void*addr,unsignedlongtext_poke_addr)+staticintmap_patch(constvoid*addr,structpatch_mapping*patch_mapping){-unsignedlongpfn;-interr;+structpage*page;+pte_tpte;+pgprot_tpgprot;if(is_vmalloc_addr(addr))-pfn=vmalloc_to_pfn(addr);+page=vmalloc_to_page(addr);else-pfn=__pa_symbol(addr)>>PAGE_SHIFT;+page=virt_to_page(addr);-err=map_kernel_page(text_poke_addr,(pfn<<PAGE_SHIFT),PAGE_KERNEL);+if(radix_enabled())+pgprot=PAGE_KERNEL;+else+pgprot=PAGE_SHARED;-pr_devel("Mapped addr %lx with pfn %lx:%d\n",text_poke_addr,pfn,err);-if(err)+patch_mapping->ptep=get_locked_pte(patching_mm,patching_addr,+&patch_mapping->ptl);+if(unlikely(!patch_mapping->ptep)){+pr_warn("map patch: failed to allocate pte for patching\n");return-1;+}++pte=mk_pte(page,pgprot);+if(!IS_ENABLED(CONFIG_PPC_BOOK3S_64))+pte=pte_mkdirty(pte);+set_pte_at(patching_mm,patching_addr,patch_mapping->ptep,pte);++init_temp_mm(&patch_mapping->temp_mm,patching_mm);+use_temporary_mm(&patch_mapping->temp_mm);return0;}-staticinlineintunmap_patch_area(unsignedlongaddr)+staticvoidunmap_patch(structpatch_mapping*patch_mapping){-pte_t*ptep;-pmd_t*pmdp;-pud_t*pudp;-pgd_t*pgdp;--pgdp=pgd_offset_k(addr);-if(unlikely(!pgdp))-return-EINVAL;--pudp=pud_offset(pgdp,addr);-if(unlikely(!pudp))-return-EINVAL;--pmdp=pmd_offset(pudp,addr);-if(unlikely(!pmdp))-return-EINVAL;--ptep=pte_offset_kernel(pmdp,addr);-if(unlikely(!ptep))-return-EINVAL;+/* In hash, pte_clear flushes the tlb */+pte_clear(patching_mm,patching_addr,patch_mapping->ptep);+unuse_temporary_mm(&patch_mapping->temp_mm);-pr_devel("clearing mm %p, pte %p, addr %lx\n",&init_mm,ptep,addr);--/*-*Inhash,pte_clearflushesthetlb,inradix,wehaveto-*/-pte_clear(&init_mm,addr,ptep);-flush_tlb_kernel_range(addr,addr+PAGE_SIZE);--return0;+/* In radix, we have to explicitly flush the tlb (no-op in hash) */+local_flush_tlb_mm(patching_mm);+pte_unmap_unlock(patch_mapping->ptep,patch_mapping->ptl);}staticintdo_patch_instruction(structppc_inst*addr,structppc_instinstr)
@@ -180,32 +138,36 @@ static int do_patch_instruction(struct ppc_inst *addr, struct ppc_inst instr)interr;structppc_inst*patch_addr=NULL;unsignedlongflags;-unsignedlongtext_poke_addr;-unsignedlongkaddr=(unsignedlong)addr;+structpatch_mappingpatch_mapping;/*-*Duringearlyearlybootpatch_instructioniscalled-*whentext_poke_areaisnotready,butwestillneed-*toallowpatching.Wejustdotheplainoldpatching+*Thepatching_mmisinitializedbeforecallingmark_rodata_ro.Prior+*tothis,patch_instructioniscalledwhenwedon'thave(anddon't+*need)thepatching_mmsojustdoplainoldpatching.*/-if(!this_cpu_read(text_poke_area))+if(!patching_mm)returnraw_patch_instruction(addr,instr);local_irq_save(flags);-text_poke_addr=(unsignedlong)__this_cpu_read(text_poke_area)->addr;-if(map_patch_area(addr,text_poke_addr)){-err=-1;+err=map_patch(addr,&patch_mapping);+if(err)gotoout;-}-patch_addr=(structppc_inst*)(text_poke_addr+(kaddr&~PAGE_MASK));+patch_addr=(structppc_inst*)(patching_addr|offset_in_page(addr));-__patch_instruction(addr,instr,patch_addr);+if(!radix_enabled())+allow_write_to_user(patch_addr,sizeof(instr));+err=__patch_instruction(addr,instr,patch_addr);+if(!radix_enabled())+prevent_write_to_user(patch_addr,sizeof(instr));-err=unmap_patch_area(text_poke_addr);-if(err)-pr_warn("failed to unmap %lx\n",text_poke_addr);+unmap_patch(&patch_mapping);+/*+*Somethingiswrongifwhatwejustwrotedoesn'tmatchwhatwe+*thinkwejustwrote.+*/+WARN_ON(!ppc_inst_equal(ppc_inst_read(addr),instr));out:local_irq_restore(flags);
From: Christopher M. Riedl <hidden> Date: 2020-06-03 05:25:01
When code patching a STRICT_KERNEL_RWX kernel the page containing the
address to be patched is temporarily mapped with permissive memory
protections. Currently, a per-cpu vmalloc patch area is used for this
purpose. While the patch area is per-cpu, the temporary page mapping is
inserted into the kernel page tables for the duration of the patching.
The mapping is exposed to CPUs other than the patching CPU - this is
undesirable from a hardening perspective.
Use the `poking_init` init hook to prepare a temporary mm and patching
address. Initialize the temporary mm by copying the init mm. Choose a
randomized patching address inside the temporary mm userspace address
portion. The next patch uses the temporary mm and patching address for
code patching.
Based on x86 implementation:
commit 4fc19708b165
("x86/alternatives: Initialize temporary mm for patching")
Signed-off-by: Christopher M. Riedl <redacted>
---
arch/powerpc/lib/code-patching.c | 33 ++++++++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
From: Christopher M. Riedl <hidden> Date: 2020-06-03 05:27:04
When live patching with STRICT_KERNEL_RWX, the CPU doing the patching
must use a temporary mapping which allows for writing to kernel text.
During the entire window of time when this temporary mapping is in use,
another CPU could write to the same mapping and maliciously alter kernel
text. Implement a LKDTM test to attempt to exploit such a openings when
a CPU is patching under STRICT_KERNEL_RWX. The test is only implemented
on powerpc for now.
The LKDTM "hijack" test works as follows:
1. A CPU executes an infinite loop to patch an instruction.
This is the "patching" CPU.
2. Another CPU attempts to write to the address of the temporary
mapping used by the "patching" CPU. This other CPU is the
"hijacker" CPU. The hijack either fails with a segfault or
succeeds, in which case some kernel text is now overwritten.
How to run the test:
mount -t debugfs none /sys/kernel/debug
(echo HIJACK_PATCH > /sys/kernel/debug/provoke-crash/DIRECT)
Signed-off-by: Christopher M. Riedl <redacted>
---
drivers/misc/lkdtm/core.c | 1 +
drivers/misc/lkdtm/lkdtm.h | 1 +
drivers/misc/lkdtm/perms.c | 101 +++++++++++++++++++++++++++++++++++++
3 files changed, 103 insertions(+)
@@ -9,6 +9,7 @@#include<linux/vmalloc.h>#include<linux/mman.h>#include<linux/uaccess.h>+#include<linux/kthread.h>#include<asm/cacheflush.h>/* Whether or not to fill the target memory area with do_nothing(). */
@@ -213,6 +214,106 @@ void lkdtm_ACCESS_NULL(void)*ptr=tmp;}+#if defined(CONFIG_PPC) && defined(CONFIG_STRICT_KERNEL_RWX)+#include<include/asm/code-patching.h>++externunsignedlongread_cpu_patching_addr(unsignedintcpu);++staticstructppc_inst*constpatch_site=(structppc_inst*)&do_nothing;++staticintlkdtm_patching_cpu(void*data)+{+interr=0;+structppc_instinsn=ppc_inst(0xdeadbeef);++pr_info("starting patching_cpu=%d\n",smp_processor_id());+do{+err=patch_instruction(patch_site,insn);+}while(ppc_inst_equal(ppc_inst_read(READ_ONCE(patch_site)),insn)&&+!err&&!kthread_should_stop());++if(err)+pr_warn("patch_instruction returned error: %d\n",err);++set_current_state(TASK_INTERRUPTIBLE);+while(!kthread_should_stop()){+schedule();+set_current_state(TASK_INTERRUPTIBLE);+}++returnerr;+}++voidlkdtm_HIJACK_PATCH(void)+{+structtask_struct*patching_kthrd;+structppc_instoriginal_insn;+intpatching_cpu,hijacker_cpu,attempts;+unsignedlongaddr;+boolhijacked;++if(num_online_cpus()<2){+pr_warn("need at least two cpus\n");+return;+}++original_insn=ppc_inst_read(READ_ONCE(patch_site));++hijacker_cpu=smp_processor_id();+patching_cpu=cpumask_any_but(cpu_online_mask,hijacker_cpu);++patching_kthrd=kthread_create_on_node(&lkdtm_patching_cpu,NULL,+cpu_to_node(patching_cpu),+"lkdtm_patching_cpu");+kthread_bind(patching_kthrd,patching_cpu);+wake_up_process(patching_kthrd);++addr=offset_in_page(patch_site)|read_cpu_patching_addr(patching_cpu);++pr_info("starting hijacker_cpu=%d\n",hijacker_cpu);+for(attempts=0;attempts<100000;++attempts){+/* Use __put_user to catch faults without an Oops */+hijacked=!__put_user(0xbad00bad,(unsignedint*)addr);++if(hijacked){+if(kthread_stop(patching_kthrd))+gotoout;+break;+}+}+pr_info("hijack attempts: %d\n",attempts);++if(hijacked){+if(*(unsignedint*)READ_ONCE(patch_site)==0xbad00bad)+pr_err("overwrote kernel text\n");+/*+*Therearewindowconditionswherethehijackercpumanagesto+*writetothepatchsitebutthesitegetsoverwrittenagainby+*thepatchingcpu.Westillconsiderthata"successful"hijack+*sincethehijackercpudidnotfaultonthewrite.+*/+pr_err("FAIL: wrote to another cpu's patching area\n");+}else{+kthread_stop(patching_kthrd);+}++out:+/* Restore the original insn for any future lkdtm tests */+patch_instruction(patch_site,original_insn);+}++#else++voidlkdtm_HIJACK_PATCH(void)+{+if(!IS_ENABLED(CONFIG_PPC))+pr_err("XFAIL: this test is powerpc-only\n");+if(!IS_ENABLED(CONFIG_STRICT_KERNEL_RWX))+pr_err("XFAIL: this test requires CONFIG_STRICT_KERNEL_RWX\n");+}++#endif /* CONFIG_PPC && CONFIG_STRICT_KERNEL_RWX */+void__initlkdtm_perms_init(void){/* Make sure we can write to __ro_after_init values during __init */
Le 03/06/2020 à 07:19, Christopher M. Riedl a écrit :
quoted hunk
x86 supports the notion of a temporary mm which restricts access to
temporary PTEs to a single CPU. A temporary mm is useful for situations
where a CPU needs to perform sensitive operations (such as patching a
STRICT_KERNEL_RWX kernel) requiring temporary mappings without exposing
said mappings to other CPUs. A side benefit is that other CPU TLBs do
not need to be flushed when the temporary mm is torn down.
Mappings in the temporary mm can be set in the userspace portion of the
address-space.
Interrupts must be disabled while the temporary mm is in use. HW
breakpoints, which may have been set by userspace as watchpoints on
addresses now within the temporary mm, are saved and disabled when
loading the temporary mm. The HW breakpoints are restored when unloading
the temporary mm. All HW breakpoints are indiscriminately disabled while
the temporary mm is in use.
Based on x86 implementation:
commit cefa929c034e
("x86/mm: Introduce temporary mm structs")
Signed-off-by: Christopher M. Riedl <redacted>
---
arch/powerpc/include/asm/debug.h | 1 +
arch/powerpc/include/asm/mmu_context.h | 64 ++++++++++++++++++++++++++
arch/powerpc/kernel/process.c | 5 ++
3 files changed, 70 insertions(+)
Le 03/06/2020 à 07:19, Christopher M. Riedl a écrit :
quoted hunk
When code patching a STRICT_KERNEL_RWX kernel the page containing the
address to be patched is temporarily mapped with permissive memory
protections. Currently, a per-cpu vmalloc patch area is used for this
purpose. While the patch area is per-cpu, the temporary page mapping is
inserted into the kernel page tables for the duration of the patching.
The mapping is exposed to CPUs other than the patching CPU - this is
undesirable from a hardening perspective.
Use the `poking_init` init hook to prepare a temporary mm and patching
address. Initialize the temporary mm by copying the init mm. Choose a
randomized patching address inside the temporary mm userspace address
portion. The next patch uses the temporary mm and patching address for
code patching.
Based on x86 implementation:
commit 4fc19708b165
("x86/alternatives: Initialize temporary mm for patching")
Signed-off-by: Christopher M. Riedl <redacted>
---
arch/powerpc/lib/code-patching.c | 33 ++++++++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
Le 03/06/2020 à 07:19, Christopher M. Riedl a écrit :
quoted hunk
Currently, code patching a STRICT_KERNEL_RWX exposes the temporary
mappings to other CPUs. These mappings should be kept local to the CPU
doing the patching. Use the pre-initialized temporary mm and patching
address for this purpose. Also add a check after patching to ensure the
patch succeeded.
Use the KUAP functions on non-BOOKS3_64 platforms since the temporary
mapping for patching uses a userspace address (to keep the mapping
local). On BOOKS3_64 platforms hash does not implement KUAP and on radix
the use of PAGE_KERNEL sets EAA[0] for the PTE which means the AMR
(KUAP) protection is ignored (see PowerISA v3.0b, Fig, 35).
Based on x86 implementation:
commit b3fd8e83ada0
("x86/alternatives: Use temporary mm for text poking")
Signed-off-by: Christopher M. Riedl <redacted>
---
arch/powerpc/lib/code-patching.c | 148 ++++++++++++-------------------
1 file changed, 55 insertions(+), 93 deletions(-)
@@ -78,101 +79,58 @@ void __init poking_init(void)pte_unmap_unlock(ptep,ptl);}-staticDEFINE_PER_CPU(structvm_struct*,text_poke_area);--staticinttext_area_cpu_up(unsignedintcpu)-{-structvm_struct*area;--area=get_vm_area(PAGE_SIZE,VM_ALLOC);-if(!area){-WARN_ONCE(1,"Failed to create text area for cpu %d\n",-cpu);-return-1;-}-this_cpu_write(text_poke_area,area);--return0;-}--staticinttext_area_cpu_down(unsignedintcpu)-{-free_vm_area(this_cpu_read(text_poke_area));-return0;-}--/*-*Runasalateinitcall.Thisallowsalltheboottimepatchingtobedone-*simplybypatchingthecode,andthenwe'recalledherepriorto-*mark_rodata_ro(),whichhappensafterallinitcallsarerun.Although-*BUG_ON()isrude,inthiscaseitshouldonlyhappenifENOMEM,andwejudge-*itasbeingpreferabletoakernelthatwillcrashlaterwhensomeonetries-*tousepatch_instruction().-*/-staticint__initsetup_text_poke_area(void)-{-BUG_ON(!cpuhp_setup_state(CPUHP_AP_ONLINE_DYN,-"powerpc/text_poke:online",text_area_cpu_up,-text_area_cpu_down));--return0;-}-late_initcall(setup_text_poke_area);+structpatch_mapping{+spinlock_t*ptl;/* for protecting pte table */+pte_t*ptep;+structtemp_mmtemp_mm;+};/**Thiscanbecalledforkerneltextoramodule.*/-staticintmap_patch_area(void*addr,unsignedlongtext_poke_addr)+staticintmap_patch(constvoid*addr,structpatch_mapping*patch_mapping){-unsignedlongpfn;-interr;+structpage*page;+pte_tpte;+pgprot_tpgprot;if(is_vmalloc_addr(addr))-pfn=vmalloc_to_pfn(addr);+page=vmalloc_to_page(addr);else-pfn=__pa_symbol(addr)>>PAGE_SHIFT;+page=virt_to_page(addr);-err=map_kernel_page(text_poke_addr,(pfn<<PAGE_SHIFT),PAGE_KERNEL);+if(radix_enabled())+pgprot=PAGE_KERNEL;+else+pgprot=PAGE_SHARED;-pr_devel("Mapped addr %lx with pfn %lx:%d\n",text_poke_addr,pfn,err);-if(err)+patch_mapping->ptep=get_locked_pte(patching_mm,patching_addr,+&patch_mapping->ptl);+if(unlikely(!patch_mapping->ptep)){+pr_warn("map patch: failed to allocate pte for patching\n");return-1;+}++pte=mk_pte(page,pgprot);+if(!IS_ENABLED(CONFIG_PPC_BOOK3S_64))+pte=pte_mkdirty(pte);
Are you should you don't need the DIRTY bit for BOOK3S/64 non radix ?
I think the DIRTY bit is needed always, and adding it when it is already
there is harmless, so it should be done inconditionnnaly.
quoted hunk
+ set_pte_at(patching_mm, patching_addr, patch_mapping->ptep, pte);
+
+ init_temp_mm(&patch_mapping->temp_mm, patching_mm);
+ use_temporary_mm(&patch_mapping->temp_mm);
return 0;
}
-static inline int unmap_patch_area(unsigned long addr)
+static void unmap_patch(struct patch_mapping *patch_mapping)
{
- pte_t *ptep;
- pmd_t *pmdp;
- pud_t *pudp;
- pgd_t *pgdp;
-
- pgdp = pgd_offset_k(addr);
- if (unlikely(!pgdp))
- return -EINVAL;
-
- pudp = pud_offset(pgdp, addr);
- if (unlikely(!pudp))
- return -EINVAL;
-
- pmdp = pmd_offset(pudp, addr);
- if (unlikely(!pmdp))
- return -EINVAL;
-
- ptep = pte_offset_kernel(pmdp, addr);
- if (unlikely(!ptep))
- return -EINVAL;
+ /* In hash, pte_clear flushes the tlb */
+ pte_clear(patching_mm, patching_addr, patch_mapping->ptep);
+ unuse_temporary_mm(&patch_mapping->temp_mm);
- pr_devel("clearing mm %p, pte %p, addr %lx\n", &init_mm, ptep, addr);
-
- /*
- * In hash, pte_clear flushes the tlb, in radix, we have to
- */
- pte_clear(&init_mm, addr, ptep);
- flush_tlb_kernel_range(addr, addr + PAGE_SIZE);
-
- return 0;
+ /* In radix, we have to explicitly flush the tlb (no-op in hash) */
+ local_flush_tlb_mm(patching_mm);
+ pte_unmap_unlock(patch_mapping->ptep, patch_mapping->ptl);
}
static int do_patch_instruction(struct ppc_inst *addr, struct ppc_inst instr)
@@ -180,32 +138,36 @@ static int do_patch_instruction(struct ppc_inst *addr, struct ppc_inst instr) int err; struct ppc_inst *patch_addr = NULL; unsigned long flags;- unsigned long text_poke_addr;- unsigned long kaddr = (unsigned long)addr;+ struct patch_mapping patch_mapping; /*- * During early early boot patch_instruction is called- * when text_poke_area is not ready, but we still need- * to allow patching. We just do the plain old patching+ * The patching_mm is initialized before calling mark_rodata_ro. Prior+ * to this, patch_instruction is called when we don't have (and don't+ * need) the patching_mm so just do plain old patching. */- if (!this_cpu_read(text_poke_area))+ if (!patching_mm) return raw_patch_instruction(addr, instr); local_irq_save(flags);- text_poke_addr = (unsigned long)__this_cpu_read(text_poke_area)->addr;- if (map_patch_area(addr, text_poke_addr)) {- err = -1;+ err = map_patch(addr, &patch_mapping);+ if (err) goto out;- }- patch_addr = (struct ppc_inst *)(text_poke_addr + (kaddr & ~PAGE_MASK));+ patch_addr = (struct ppc_inst *)(patching_addr | offset_in_page(addr));- __patch_instruction(addr, instr, patch_addr);+ if (!radix_enabled())+ allow_write_to_user(patch_addr, sizeof(instr));
Can't use sizeof(instr), you have to use ppc_inst_size()
- err = unmap_patch_area(text_poke_addr);
- if (err)
- pr_warn("failed to unmap %lx\n", text_poke_addr);
+ unmap_patch(&patch_mapping);
+ /*
+ * Something is wrong if what we just wrote doesn't match what we
+ * think we just wrote.
+ */
+ WARN_ON(!ppc_inst_equal(ppc_inst_read(addr), instr));
out:
local_irq_restore(flags);
Le 03/06/2020 à 07:19, Christopher M. Riedl a écrit :
quoted hunk
When live patching a STRICT_RWX kernel, a mapping is installed at a
"patching address" with temporary write permissions. Provide a
LKDTM-only accessor function for this address in preparation for a LKDTM
test which attempts to "hijack" this mapping by writing to it from
another CPU.
Signed-off-by: Christopher M. Riedl <redacted>
---
arch/powerpc/lib/code-patching.c | 7 +++++++
1 file changed, 7 insertions(+)
Le 03/06/2020 à 07:19, Christopher M. Riedl a écrit :
quoted hunk
When live patching with STRICT_KERNEL_RWX, the CPU doing the patching
must use a temporary mapping which allows for writing to kernel text.
During the entire window of time when this temporary mapping is in use,
another CPU could write to the same mapping and maliciously alter kernel
text. Implement a LKDTM test to attempt to exploit such a openings when
a CPU is patching under STRICT_KERNEL_RWX. The test is only implemented
on powerpc for now.
The LKDTM "hijack" test works as follows:
1. A CPU executes an infinite loop to patch an instruction.
This is the "patching" CPU.
2. Another CPU attempts to write to the address of the temporary
mapping used by the "patching" CPU. This other CPU is the
"hijacker" CPU. The hijack either fails with a segfault or
succeeds, in which case some kernel text is now overwritten.
How to run the test:
mount -t debugfs none /sys/kernel/debug
(echo HIJACK_PATCH > /sys/kernel/debug/provoke-crash/DIRECT)
Signed-off-by: Christopher M. Riedl <redacted>
---
drivers/misc/lkdtm/core.c | 1 +
drivers/misc/lkdtm/lkdtm.h | 1 +
drivers/misc/lkdtm/perms.c | 101 +++++++++++++++++++++++++++++++++++++
3 files changed, 103 insertions(+)
@@ -9,6 +9,7 @@#include<linux/vmalloc.h>#include<linux/mman.h>#include<linux/uaccess.h>+#include<linux/kthread.h>#include<asm/cacheflush.h>/* Whether or not to fill the target memory area with do_nothing(). */
Why only PPC ? I understood that this applies also to x86. And
regarless, the test should be able to run on other architectures,
allthought for sure it will fail. That's the case for other tests.
+#include <include/asm/code-patching.h>
+
+extern unsigned long read_cpu_patching_addr(unsigned int cpu);
'extern' keyword is useless for functions and shall be banned.
Shouldn't this declaration be in asm/code-patching.h ?
+
+static struct ppc_inst * const patch_site = (struct ppc_inst *)&do_nothing;
+
+static int lkdtm_patching_cpu(void *data)
+{
+ int err = 0;
+ struct ppc_inst insn = ppc_inst(0xdeadbeef);
+
+ pr_info("starting patching_cpu=%d\n", smp_processor_id());
+ do {
+ err = patch_instruction(patch_site, insn);
+ } while (ppc_inst_equal(ppc_inst_read(READ_ONCE(patch_site)), insn) &&
+ !err && !kthread_should_stop());
+
+ if (err)
+ pr_warn("patch_instruction returned error: %d\n", err);
+
+ set_current_state(TASK_INTERRUPTIBLE);
+ while (!kthread_should_stop()) {
+ schedule();
+ set_current_state(TASK_INTERRUPTIBLE);
+ }
+
+ return err;
+}
+
+void lkdtm_HIJACK_PATCH(void)
+{
+ struct task_struct *patching_kthrd;
+ struct ppc_inst original_insn;
+ int patching_cpu, hijacker_cpu, attempts;
+ unsigned long addr;
+ bool hijacked;
+
+ if (num_online_cpus() < 2) {
+ pr_warn("need at least two cpus\n");
+ return;
+ }
+
+ original_insn = ppc_inst_read(READ_ONCE(patch_site));
+
+ hijacker_cpu = smp_processor_id();
+ patching_cpu = cpumask_any_but(cpu_online_mask, hijacker_cpu);
+
+ patching_kthrd = kthread_create_on_node(&lkdtm_patching_cpu, NULL,
+ cpu_to_node(patching_cpu),
+ "lkdtm_patching_cpu");
+ kthread_bind(patching_kthrd, patching_cpu);
+ wake_up_process(patching_kthrd);
+
+ addr = offset_in_page(patch_site) | read_cpu_patching_addr(patching_cpu);
+
+ pr_info("starting hijacker_cpu=%d\n", hijacker_cpu);
+ for (attempts = 0; attempts < 100000; ++attempts) {
+ /* Use __put_user to catch faults without an Oops */
+ hijacked = !__put_user(0xbad00bad, (unsigned int *)addr);
+
+ if (hijacked) {
+ if (kthread_stop(patching_kthrd))
+ goto out;
+ break;
+ }
+ }
+ pr_info("hijack attempts: %d\n", attempts);
+
+ if (hijacked) {
+ if (*(unsigned int *)READ_ONCE(patch_site) == 0xbad00bad)
+ pr_err("overwrote kernel text\n");
+ /*
+ * There are window conditions where the hijacker cpu manages to
+ * write to the patch site but the site gets overwritten again by
+ * the patching cpu. We still consider that a "successful" hijack
+ * since the hijacker cpu did not fault on the write.
+ */
+ pr_err("FAIL: wrote to another cpu's patching area\n");
+ } else {
+ kthread_stop(patching_kthrd);
+ }
+
+out:
+ /* Restore the original insn for any future lkdtm tests */
+ patch_instruction(patch_site, original_insn);
+}
+
+#else
+
+void lkdtm_HIJACK_PATCH(void)
+{
+ if (!IS_ENABLED(CONFIG_PPC))
+ pr_err("XFAIL: this test is powerpc-only\n");
+ if (!IS_ENABLED(CONFIG_STRICT_KERNEL_RWX))
+ pr_err("XFAIL: this test requires CONFIG_STRICT_KERNEL_RWX\n");
+}
+
+#endif /* CONFIG_PPC && CONFIG_STRICT_KERNEL_RWX */
+
void __init lkdtm_perms_init(void)
{
/* Make sure we can write to __ro_after_init values during __init */
From: Christopher M. Riedl <hidden> Date: 2020-06-09 04:40:41
On Wed Jun 3, 2020 at 9:14 AM, Christophe Leroy wrote:
Le 03/06/2020 à 07:19, Christopher M. Riedl a écrit :
quoted
When live patching a STRICT_RWX kernel, a mapping is installed at a
"patching address" with temporary write permissions. Provide a
LKDTM-only accessor function for this address in preparation for a LKDTM
test which attempts to "hijack" this mapping by writing to it from
another CPU.
Signed-off-by: Christopher M. Riedl <redacted>
---
arch/powerpc/lib/code-patching.c | 7 +++++++
1 file changed, 7 insertions(+)
From: Christopher M. Riedl <hidden> Date: 2020-06-09 04:48:23
On Wed Jun 3, 2020 at 9:20 AM, Christophe Leroy wrote:
Le 03/06/2020 à 07:19, Christopher M. Riedl a écrit :
quoted
When live patching with STRICT_KERNEL_RWX, the CPU doing the patching
must use a temporary mapping which allows for writing to kernel text.
During the entire window of time when this temporary mapping is in use,
another CPU could write to the same mapping and maliciously alter kernel
text. Implement a LKDTM test to attempt to exploit such a openings when
a CPU is patching under STRICT_KERNEL_RWX. The test is only implemented
on powerpc for now.
The LKDTM "hijack" test works as follows:
1. A CPU executes an infinite loop to patch an instruction.
This is the "patching" CPU.
2. Another CPU attempts to write to the address of the temporary
mapping used by the "patching" CPU. This other CPU is the
"hijacker" CPU. The hijack either fails with a segfault or
succeeds, in which case some kernel text is now overwritten.
How to run the test:
mount -t debugfs none /sys/kernel/debug
(echo HIJACK_PATCH > /sys/kernel/debug/provoke-crash/DIRECT)
Signed-off-by: Christopher M. Riedl <redacted>
---
drivers/misc/lkdtm/core.c | 1 +
drivers/misc/lkdtm/lkdtm.h | 1 +
drivers/misc/lkdtm/perms.c | 101 +++++++++++++++++++++++++++++++++++++
3 files changed, 103 insertions(+)
@@ -9,6 +9,7 @@#include<linux/vmalloc.h>#include<linux/mman.h>#include<linux/uaccess.h>+#include<linux/kthread.h>#include<asm/cacheflush.h>/* Whether or not to fill the target memory area with do_nothing(). */
Why only PPC ? I understood that this applies also to x86. And
regarless, the test should be able to run on other architectures,
allthought for sure it will fail. That's the case for other tests.
I think the code patching details are different between architectures
and (for now) I am only comfortable enough with PPC to implement
something meaningful. The intent of the RFC versions was to try to get
some interest (hence the distribution to the hardening list) or feedback
about how this could work on other architectures.
There are a few other tests which are arch specific in LKDTM so it's not
completely unheard of :)
quoted
+#include <include/asm/code-patching.h>
+
+extern unsigned long read_cpu_patching_addr(unsigned int cpu);
'extern' keyword is useless for functions and shall be banned.
Shouldn't this declaration be in asm/code-patching.h ?
Yes, left-over from the RFC version, this will be fixed in the next
spin.
quoted
+
+static struct ppc_inst * const patch_site = (struct ppc_inst *)&do_nothing;
+
+static int lkdtm_patching_cpu(void *data)
+{
+ int err = 0;
+ struct ppc_inst insn = ppc_inst(0xdeadbeef);
+
+ pr_info("starting patching_cpu=%d\n", smp_processor_id());
+ do {
+ err = patch_instruction(patch_site, insn);
+ } while (ppc_inst_equal(ppc_inst_read(READ_ONCE(patch_site)), insn) &&
+ !err && !kthread_should_stop());
+
+ if (err)
+ pr_warn("patch_instruction returned error: %d\n", err);
+
+ set_current_state(TASK_INTERRUPTIBLE);
+ while (!kthread_should_stop()) {
+ schedule();
+ set_current_state(TASK_INTERRUPTIBLE);
+ }
+
+ return err;
+}
+
+void lkdtm_HIJACK_PATCH(void)
+{
+ struct task_struct *patching_kthrd;
+ struct ppc_inst original_insn;
+ int patching_cpu, hijacker_cpu, attempts;
+ unsigned long addr;
+ bool hijacked;
+
+ if (num_online_cpus() < 2) {
+ pr_warn("need at least two cpus\n");
+ return;
+ }
+
+ original_insn = ppc_inst_read(READ_ONCE(patch_site));
+
+ hijacker_cpu = smp_processor_id();
+ patching_cpu = cpumask_any_but(cpu_online_mask, hijacker_cpu);
+
+ patching_kthrd = kthread_create_on_node(&lkdtm_patching_cpu, NULL,
+ cpu_to_node(patching_cpu),
+ "lkdtm_patching_cpu");
+ kthread_bind(patching_kthrd, patching_cpu);
+ wake_up_process(patching_kthrd);
+
+ addr = offset_in_page(patch_site) | read_cpu_patching_addr(patching_cpu);
+
+ pr_info("starting hijacker_cpu=%d\n", hijacker_cpu);
+ for (attempts = 0; attempts < 100000; ++attempts) {
+ /* Use __put_user to catch faults without an Oops */
+ hijacked = !__put_user(0xbad00bad, (unsigned int *)addr);
+
+ if (hijacked) {
+ if (kthread_stop(patching_kthrd))
+ goto out;
+ break;
+ }
+ }
+ pr_info("hijack attempts: %d\n", attempts);
+
+ if (hijacked) {
+ if (*(unsigned int *)READ_ONCE(patch_site) == 0xbad00bad)
+ pr_err("overwrote kernel text\n");
+ /*
+ * There are window conditions where the hijacker cpu manages to
+ * write to the patch site but the site gets overwritten again by
+ * the patching cpu. We still consider that a "successful" hijack
+ * since the hijacker cpu did not fault on the write.
+ */
+ pr_err("FAIL: wrote to another cpu's patching area\n");
+ } else {
+ kthread_stop(patching_kthrd);
+ }
+
+out:
+ /* Restore the original insn for any future lkdtm tests */
+ patch_instruction(patch_site, original_insn);
+}
+
+#else
+
+void lkdtm_HIJACK_PATCH(void)
+{
+ if (!IS_ENABLED(CONFIG_PPC))
+ pr_err("XFAIL: this test is powerpc-only\n");
+ if (!IS_ENABLED(CONFIG_STRICT_KERNEL_RWX))
+ pr_err("XFAIL: this test requires CONFIG_STRICT_KERNEL_RWX\n");
+}
+
+#endif /* CONFIG_PPC && CONFIG_STRICT_KERNEL_RWX */
+
void __init lkdtm_perms_init(void)
{
/* Make sure we can write to __ro_after_init values during __init */
From: Christopher M. Riedl <hidden> Date: 2020-06-11 03:29:01
On Wed Jun 3, 2020 at 9:01 AM, Christophe Leroy wrote:
Le 03/06/2020 à 07:19, Christopher M. Riedl a écrit :
quoted
When code patching a STRICT_KERNEL_RWX kernel the page containing the
address to be patched is temporarily mapped with permissive memory
protections. Currently, a per-cpu vmalloc patch area is used for this
purpose. While the patch area is per-cpu, the temporary page mapping is
inserted into the kernel page tables for the duration of the patching.
The mapping is exposed to CPUs other than the patching CPU - this is
undesirable from a hardening perspective.
Use the `poking_init` init hook to prepare a temporary mm and patching
address. Initialize the temporary mm by copying the init mm. Choose a
randomized patching address inside the temporary mm userspace address
portion. The next patch uses the temporary mm and patching address for
code patching.
Based on x86 implementation:
commit 4fc19708b165
("x86/alternatives: Initialize temporary mm for patching")
Signed-off-by: Christopher M. Riedl <redacted>
---
arch/powerpc/lib/code-patching.c | 33 ++++++++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
From: Christopher M. Riedl <hidden> Date: 2020-06-11 03:31:47
On Wed Jun 3, 2020 at 9:12 AM, Christophe Leroy wrote:
Le 03/06/2020 à 07:19, Christopher M. Riedl a écrit :
quoted
Currently, code patching a STRICT_KERNEL_RWX exposes the temporary
mappings to other CPUs. These mappings should be kept local to the CPU
doing the patching. Use the pre-initialized temporary mm and patching
address for this purpose. Also add a check after patching to ensure the
patch succeeded.
Use the KUAP functions on non-BOOKS3_64 platforms since the temporary
mapping for patching uses a userspace address (to keep the mapping
local). On BOOKS3_64 platforms hash does not implement KUAP and on radix
the use of PAGE_KERNEL sets EAA[0] for the PTE which means the AMR
(KUAP) protection is ignored (see PowerISA v3.0b, Fig, 35).
Based on x86 implementation:
commit b3fd8e83ada0
("x86/alternatives: Use temporary mm for text poking")
Signed-off-by: Christopher M. Riedl <redacted>
---
arch/powerpc/lib/code-patching.c | 148 ++++++++++++-------------------
1 file changed, 55 insertions(+), 93 deletions(-)
@@ -78,101 +79,58 @@ void __init poking_init(void)pte_unmap_unlock(ptep,ptl);}-staticDEFINE_PER_CPU(structvm_struct*,text_poke_area);--staticinttext_area_cpu_up(unsignedintcpu)-{-structvm_struct*area;--area=get_vm_area(PAGE_SIZE,VM_ALLOC);-if(!area){-WARN_ONCE(1,"Failed to create text area for cpu %d\n",-cpu);-return-1;-}-this_cpu_write(text_poke_area,area);--return0;-}--staticinttext_area_cpu_down(unsignedintcpu)-{-free_vm_area(this_cpu_read(text_poke_area));-return0;-}--/*-*Runasalateinitcall.Thisallowsalltheboottimepatchingtobedone-*simplybypatchingthecode,andthenwe'recalledherepriorto-*mark_rodata_ro(),whichhappensafterallinitcallsarerun.Although-*BUG_ON()isrude,inthiscaseitshouldonlyhappenifENOMEM,andwejudge-*itasbeingpreferabletoakernelthatwillcrashlaterwhensomeonetries-*tousepatch_instruction().-*/-staticint__initsetup_text_poke_area(void)-{-BUG_ON(!cpuhp_setup_state(CPUHP_AP_ONLINE_DYN,-"powerpc/text_poke:online",text_area_cpu_up,-text_area_cpu_down));--return0;-}-late_initcall(setup_text_poke_area);+structpatch_mapping{+spinlock_t*ptl;/* for protecting pte table */+pte_t*ptep;+structtemp_mmtemp_mm;+};/**Thiscanbecalledforkerneltextoramodule.*/-staticintmap_patch_area(void*addr,unsignedlongtext_poke_addr)+staticintmap_patch(constvoid*addr,structpatch_mapping*patch_mapping){-unsignedlongpfn;-interr;+structpage*page;+pte_tpte;+pgprot_tpgprot;if(is_vmalloc_addr(addr))-pfn=vmalloc_to_pfn(addr);+page=vmalloc_to_page(addr);else-pfn=__pa_symbol(addr)>>PAGE_SHIFT;+page=virt_to_page(addr);-err=map_kernel_page(text_poke_addr,(pfn<<PAGE_SHIFT),PAGE_KERNEL);+if(radix_enabled())+pgprot=PAGE_KERNEL;+else+pgprot=PAGE_SHARED;-pr_devel("Mapped addr %lx with pfn %lx:%d\n",text_poke_addr,pfn,err);-if(err)+patch_mapping->ptep=get_locked_pte(patching_mm,patching_addr,+&patch_mapping->ptl);+if(unlikely(!patch_mapping->ptep)){+pr_warn("map patch: failed to allocate pte for patching\n");return-1;+}++pte=mk_pte(page,pgprot);+if(!IS_ENABLED(CONFIG_PPC_BOOK3S_64))+pte=pte_mkdirty(pte);
Are you should you don't need the DIRTY bit for BOOK3S/64 non radix ?
I think the DIRTY bit is needed always, and adding it when it is already
there is harmless, so it should be done inconditionnnaly.
I tested this and it doesn't seem to make a differnce so I can make this
common in the next spin.
quoted
+ set_pte_at(patching_mm, patching_addr, patch_mapping->ptep, pte);
+
+ init_temp_mm(&patch_mapping->temp_mm, patching_mm);
+ use_temporary_mm(&patch_mapping->temp_mm);
return 0;
}
-static inline int unmap_patch_area(unsigned long addr)
+static void unmap_patch(struct patch_mapping *patch_mapping)
{
- pte_t *ptep;
- pmd_t *pmdp;
- pud_t *pudp;
- pgd_t *pgdp;
-
- pgdp = pgd_offset_k(addr);
- if (unlikely(!pgdp))
- return -EINVAL;
-
- pudp = pud_offset(pgdp, addr);
- if (unlikely(!pudp))
- return -EINVAL;
-
- pmdp = pmd_offset(pudp, addr);
- if (unlikely(!pmdp))
- return -EINVAL;
-
- ptep = pte_offset_kernel(pmdp, addr);
- if (unlikely(!ptep))
- return -EINVAL;
+ /* In hash, pte_clear flushes the tlb */
+ pte_clear(patching_mm, patching_addr, patch_mapping->ptep);
+ unuse_temporary_mm(&patch_mapping->temp_mm);
- pr_devel("clearing mm %p, pte %p, addr %lx\n", &init_mm, ptep, addr);
-
- /*
- * In hash, pte_clear flushes the tlb, in radix, we have to
- */
- pte_clear(&init_mm, addr, ptep);
- flush_tlb_kernel_range(addr, addr + PAGE_SIZE);
-
- return 0;
+ /* In radix, we have to explicitly flush the tlb (no-op in hash) */
+ local_flush_tlb_mm(patching_mm);
+ pte_unmap_unlock(patch_mapping->ptep, patch_mapping->ptl);
}
static int do_patch_instruction(struct ppc_inst *addr, struct ppc_inst instr)
@@ -180,32 +138,36 @@ static int do_patch_instruction(struct ppc_inst *addr, struct ppc_inst instr) int err; struct ppc_inst *patch_addr = NULL; unsigned long flags;- unsigned long text_poke_addr;- unsigned long kaddr = (unsigned long)addr;+ struct patch_mapping patch_mapping; /*- * During early early boot patch_instruction is called- * when text_poke_area is not ready, but we still need- * to allow patching. We just do the plain old patching+ * The patching_mm is initialized before calling mark_rodata_ro. Prior+ * to this, patch_instruction is called when we don't have (and don't+ * need) the patching_mm so just do plain old patching. */- if (!this_cpu_read(text_poke_area))+ if (!patching_mm) return raw_patch_instruction(addr, instr); local_irq_save(flags);- text_poke_addr = (unsigned long)__this_cpu_read(text_poke_area)->addr;- if (map_patch_area(addr, text_poke_addr)) {- err = -1;+ err = map_patch(addr, &patch_mapping);+ if (err) goto out;- }- patch_addr = (struct ppc_inst *)(text_poke_addr + (kaddr & ~PAGE_MASK));+ patch_addr = (struct ppc_inst *)(patching_addr | offset_in_page(addr));- __patch_instruction(addr, instr, patch_addr);+ if (!radix_enabled())+ allow_write_to_user(patch_addr, sizeof(instr));
Can't use sizeof(instr), you have to use ppc_inst_size()
Good catch, will fix this in the next spin (the other one below too).
- err = unmap_patch_area(text_poke_addr);
- if (err)
- pr_warn("failed to unmap %lx\n", text_poke_addr);
+ unmap_patch(&patch_mapping);
+ /*
+ * Something is wrong if what we just wrote doesn't match what we
+ * think we just wrote.
+ */
+ WARN_ON(!ppc_inst_equal(ppc_inst_read(addr), instr));
out:
local_irq_restore(flags);
From: Christopher M. Riedl <hidden> Date: 2020-06-11 03:34:47
On Wed Jun 3, 2020 at 8:58 AM, Christophe Leroy wrote:
Le 03/06/2020 à 07:19, Christopher M. Riedl a écrit :
quoted
x86 supports the notion of a temporary mm which restricts access to
temporary PTEs to a single CPU. A temporary mm is useful for situations
where a CPU needs to perform sensitive operations (such as patching a
STRICT_KERNEL_RWX kernel) requiring temporary mappings without exposing
said mappings to other CPUs. A side benefit is that other CPU TLBs do
not need to be flushed when the temporary mm is torn down.
Mappings in the temporary mm can be set in the userspace portion of the
address-space.
Interrupts must be disabled while the temporary mm is in use. HW
breakpoints, which may have been set by userspace as watchpoints on
addresses now within the temporary mm, are saved and disabled when
loading the temporary mm. The HW breakpoints are restored when unloading
the temporary mm. All HW breakpoints are indiscriminately disabled while
the temporary mm is in use.
Based on x86 implementation:
commit cefa929c034e
("x86/mm: Introduce temporary mm structs")
Signed-off-by: Christopher M. Riedl <redacted>
---
arch/powerpc/include/asm/debug.h | 1 +
arch/powerpc/include/asm/mmu_context.h | 64 ++++++++++++++++++++++++++
arch/powerpc/kernel/process.c | 5 ++
3 files changed, 70 insertions(+)
Le 03/06/2020 à 07:19, Christopher M. Riedl a écrit :
quoted hunk
When live patching with STRICT_KERNEL_RWX, the CPU doing the patching
must use a temporary mapping which allows for writing to kernel text.
During the entire window of time when this temporary mapping is in use,
another CPU could write to the same mapping and maliciously alter kernel
text. Implement a LKDTM test to attempt to exploit such a openings when
a CPU is patching under STRICT_KERNEL_RWX. The test is only implemented
on powerpc for now.
The LKDTM "hijack" test works as follows:
1. A CPU executes an infinite loop to patch an instruction.
This is the "patching" CPU.
2. Another CPU attempts to write to the address of the temporary
mapping used by the "patching" CPU. This other CPU is the
"hijacker" CPU. The hijack either fails with a segfault or
succeeds, in which case some kernel text is now overwritten.
How to run the test:
mount -t debugfs none /sys/kernel/debug
(echo HIJACK_PATCH > /sys/kernel/debug/provoke-crash/DIRECT)
Signed-off-by: Christopher M. Riedl <redacted>
---
drivers/misc/lkdtm/core.c | 1 +
drivers/misc/lkdtm/lkdtm.h | 1 +
drivers/misc/lkdtm/perms.c | 101 +++++++++++++++++++++++++++++++++++++
3 files changed, 103 insertions(+)
@@ -9,6 +9,7 @@#include<linux/vmalloc.h>#include<linux/mman.h>#include<linux/uaccess.h>+#include<linux/kthread.h>#include<asm/cacheflush.h>/* Whether or not to fill the target memory area with do_nothing(). */