Linux version: v6.0
Architecture: ARMv8
Introduction
I have heard of KASLR (Kernel Address Space Layout Randomization) for a long time now, and had always find it interesting. However because KASLR isn’t really a core operating system concept, I never got the chance to research it other than reading some random articles online that only discusses the topic conceptually. The deep knowledge of how it actually works had always been missing.
I recently got some time to deeply investigate this cool feature of Linux, I will split what I found in two blogs, first explaining the implementation of the kernel image randomization, the second explaining the randomization of the linear mapping.
It is advised to learn about the boot flow of the Linux ARM64 kernel before proceeding.
KASLR Implementation
KASLR is closely related to Linux ARM64’s boot sequence, the execution context will be breifly presented, then the code explanation will start from the early function __primary_switch
.
Execution Context
The boot (primary) CPU runs to the __primary_switch
with the MMU not yet enabled, with CPU system registers initialized, identity map page tables initialized. Also with x22
storing the physical address of the FDT (Flat Device Tree).
__primary_switch
__primary_switch
means “primary CPU switch on MMU”
1 | SYM_FUNC_START_LOCAL(__primary_switch) |
kaslr_early_init
The Makefile which arch/arm64/kernel/pi/kaslr_early.c
uses, which kaslr_early_init
resides in, prepends all symbols with __pi_
, therefore the bl __pi_kaslr_early_init
call gets directed here.
1 | asmlinkage u64 kaslr_early_init(void *fdt) |
__relocate_kernel
In the code explanation below, I removed the part in the CONFIG_RELR
option, because I decided to skip that part and also defconfig
does not use it.
This is the critical function for KASLR, it relocates the kernel at runtime. The kernel build process produces a relocation entry for each global symbol access, by including the entries in the kernel image, it allows the kernel to read the entries and relocate itself at runtime.
This reference contains information about ARM relocation entry types, only one type is used here.
The format of ELF relocation entry looks like:
1 | typedef struct { |
KASLR only processes one type of relocation that is R_AARCH64_RELATIVE
, it requires writing “r_addend + random offset (address difference between run time and compile time)
“ to the address corresponding to r_offset
at run time. For example assume there’s an instruction trying to load symbol sym
‘s address into register x0
, the compiler emits ldr x0, #a0
, #a0
here is just for demoing purpose. Assume sym
‘s compile time address is 0x500
, the instruction’s compile time address is 0x1000
, the relocation generated would be: r_offset = 0x10a0
, r_addend = 0x500
. Further assume the instruction’s run time address is 0x3000
, (so sym
‘s run time address is 0x2500
), the random offset is 0x2000
. What we need to do to relocate is read r_addend (0x500)
, add it with 0x2000
(the offset), then save the result into r_offset
+ (the offset), that is writing 0x2500
into 0x10a0 + 0x2000 = 0x30a0
. Now when the instruciton is run x0
would be assigned 0x2500
, which is the correct run time address of sym
.
1 | SYM_FUNC_START_LOCAL(__relocate_kernel) |
Ending of __primary_switch
A global address load happens right after __relocation kernel
returns:
1 | // this load instruction generates a relocation entry which is relocated |
Linker Options
The ARM64 Makefile contains linker options for KASLR:
1 | ifeq ($(CONFIG_RELOCATABLE), y) |
Four options are used:
—no-apply-dynamic-relcos
seems to be related to the skippedCONFIG_RELR
, but I’m not sure-shared
means to create a shared library-Bsymbolic
means to bind references to global symbols to the definition within the shared library, if any. I think this preventsld
from emitting relocation entries that is used for external symbols.-z notext
: not sure about this one
Some Questions
The code analysis above is pretty thorough, but a lot of further questions are raised as well, hope I will be able to answer them as I learn more.
What are the actual differences when we add the four linker options? What if we don’t add them?
Originally I thought all of the relocation entry would disappear after turning off KASLR, but they still persisted, this doesn’t affect the kernel from running properly but doesn’t this waste some space?
How does the build process limit the relocation entry to only emit the
R_AARCH64_RELATIVE
type? What circumstances generate different relocation types?