| // SPDX-License-Identifier: GPL-2.0-only | |
| /* | |
| * Copyright (C) 2022 - Google LLC | |
| * Author: Ard Biesheuvel <ardb@google.com> | |
| */ | |
| #include <linux/errno.h> | |
| #include <linux/init.h> | |
| #include <linux/linkage.h> | |
| #include <linux/types.h> | |
| #include <asm/scs.h> | |
| #include "pi.h" | |
| bool dynamic_scs_is_enabled; | |
| // | |
| // This minimal DWARF CFI parser is partially based on the code in | |
| // arch/arc/kernel/unwind.c, and on the document below: | |
| // https://refspecs.linuxbase.org/LSB_4.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html | |
| // | |
| #define DW_CFA_nop 0x00 | |
| #define DW_CFA_set_loc 0x01 | |
| #define DW_CFA_advance_loc1 0x02 | |
| #define DW_CFA_advance_loc2 0x03 | |
| #define DW_CFA_advance_loc4 0x04 | |
| #define DW_CFA_offset_extended 0x05 | |
| #define DW_CFA_restore_extended 0x06 | |
| #define DW_CFA_undefined 0x07 | |
| #define DW_CFA_same_value 0x08 | |
| #define DW_CFA_register 0x09 | |
| #define DW_CFA_remember_state 0x0a | |
| #define DW_CFA_restore_state 0x0b | |
| #define DW_CFA_def_cfa 0x0c | |
| #define DW_CFA_def_cfa_register 0x0d | |
| #define DW_CFA_def_cfa_offset 0x0e | |
| #define DW_CFA_def_cfa_expression 0x0f | |
| #define DW_CFA_expression 0x10 | |
| #define DW_CFA_offset_extended_sf 0x11 | |
| #define DW_CFA_def_cfa_sf 0x12 | |
| #define DW_CFA_def_cfa_offset_sf 0x13 | |
| #define DW_CFA_val_offset 0x14 | |
| #define DW_CFA_val_offset_sf 0x15 | |
| #define DW_CFA_val_expression 0x16 | |
| #define DW_CFA_lo_user 0x1c | |
| #define DW_CFA_negate_ra_state 0x2d | |
| #define DW_CFA_GNU_args_size 0x2e | |
| #define DW_CFA_GNU_negative_offset_extended 0x2f | |
| #define DW_CFA_hi_user 0x3f | |
| #define DW_EH_PE_sdata4 0x0b | |
| #define DW_EH_PE_sdata8 0x0c | |
| #define DW_EH_PE_pcrel 0x10 | |
| enum { | |
| PACIASP = 0xd503233f, | |
| AUTIASP = 0xd50323bf, | |
| SCS_PUSH = 0xf800865e, | |
| SCS_POP = 0xf85f8e5e, | |
| }; | |
| static void __always_inline scs_patch_loc(u64 loc) | |
| { | |
| u32 insn = le32_to_cpup((void *)loc); | |
| switch (insn) { | |
| case PACIASP: | |
| *(u32 *)loc = cpu_to_le32(SCS_PUSH); | |
| break; | |
| case AUTIASP: | |
| *(u32 *)loc = cpu_to_le32(SCS_POP); | |
| break; | |
| default: | |
| /* | |
| * While the DW_CFA_negate_ra_state directive is guaranteed to | |
| * appear right after a PACIASP/AUTIASP instruction, it may | |
| * also appear after a DW_CFA_restore_state directive that | |
| * restores a state that is only partially accurate, and is | |
| * followed by DW_CFA_negate_ra_state directive to toggle the | |
| * PAC bit again. So we permit other instructions here, and ignore | |
| * them. | |
| */ | |
| return; | |
| } | |
| if (IS_ENABLED(CONFIG_ARM64_WORKAROUND_CLEAN_CACHE)) | |
| asm("dc civac, %0" :: "r"(loc)); | |
| else | |
| asm(ALTERNATIVE("dc cvau, %0", "nop", ARM64_HAS_CACHE_IDC) | |
| :: "r"(loc)); | |
| } | |
| /* | |
| * Skip one uleb128/sleb128 encoded quantity from the opcode stream. All bytes | |
| * except the last one have bit #7 set. | |
| */ | |
| static int __always_inline skip_xleb128(const u8 **opcode, int size) | |
| { | |
| u8 c; | |
| do { | |
| c = *(*opcode)++; | |
| size--; | |
| } while (c & BIT(7)); | |
| return size; | |
| } | |
| struct eh_frame { | |
| /* | |
| * The size of this frame if 0 < size < U32_MAX, 0 terminates the list. | |
| */ | |
| u32 size; | |
| /* | |
| * The first frame is a Common Information Entry (CIE) frame, followed | |
| * by one or more Frame Description Entry (FDE) frames. In the former | |
| * case, this field is 0, otherwise it is the negated offset relative | |
| * to the associated CIE frame. | |
| */ | |
| u32 cie_id_or_pointer; | |
| union { | |
| struct { // CIE | |
| u8 version; | |
| u8 augmentation_string[3]; | |
| u8 code_alignment_factor; | |
| u8 data_alignment_factor; | |
| u8 return_address_register; | |
| u8 augmentation_data_size; | |
| u8 fde_pointer_format; | |
| }; | |
| struct { // FDE | |
| s32 initial_loc; | |
| s32 range; | |
| u8 opcodes[]; | |
| }; | |
| struct { // FDE | |
| s64 initial_loc64; | |
| s64 range64; | |
| u8 opcodes64[]; | |
| }; | |
| }; | |
| }; | |
| static int scs_handle_fde_frame(const struct eh_frame *frame, | |
| int code_alignment_factor, | |
| bool use_sdata8, | |
| bool dry_run) | |
| { | |
| int size = frame->size - offsetof(struct eh_frame, opcodes) + 4; | |
| u64 loc = (u64)offset_to_ptr(&frame->initial_loc); | |
| const u8 *opcode = frame->opcodes; | |
| int l; | |
| if (use_sdata8) { | |
| loc = (u64)&frame->initial_loc64 + frame->initial_loc64; | |
| opcode = frame->opcodes64; | |
| size -= 8; | |
| } | |
| // assume single byte uleb128_t for augmentation data size | |
| if (*opcode & BIT(7)) | |
| return EDYNSCS_INVALID_FDE_AUGM_DATA_SIZE; | |
| l = *opcode++; | |
| opcode += l; | |
| size -= l + 1; | |
| /* | |
| * Starting from 'loc', apply the CFA opcodes that advance the location | |
| * pointer, and identify the locations of the PAC instructions. | |
| */ | |
| while (size-- > 0) { | |
| switch (*opcode++) { | |
| case DW_CFA_nop: | |
| case DW_CFA_remember_state: | |
| case DW_CFA_restore_state: | |
| break; | |
| case DW_CFA_advance_loc1: | |
| loc += *opcode++ * code_alignment_factor; | |
| size--; | |
| break; | |
| case DW_CFA_advance_loc2: | |
| loc += *opcode++ * code_alignment_factor; | |
| loc += (*opcode++ << 8) * code_alignment_factor; | |
| size -= 2; | |
| break; | |
| case DW_CFA_advance_loc4: | |
| loc += *opcode++ * code_alignment_factor; | |
| loc += (*opcode++ << 8) * code_alignment_factor; | |
| loc += (*opcode++ << 16) * code_alignment_factor; | |
| loc += ((u64)*opcode++ << 24) * code_alignment_factor; | |
| size -= 4; | |
| break; | |
| case DW_CFA_def_cfa: | |
| case DW_CFA_offset_extended: | |
| size = skip_xleb128(&opcode, size); | |
| fallthrough; | |
| case DW_CFA_def_cfa_offset: | |
| case DW_CFA_def_cfa_offset_sf: | |
| case DW_CFA_def_cfa_register: | |
| case DW_CFA_same_value: | |
| case DW_CFA_restore_extended: | |
| case 0x80 ... 0xbf: | |
| size = skip_xleb128(&opcode, size); | |
| break; | |
| case DW_CFA_negate_ra_state: | |
| if (!dry_run) | |
| scs_patch_loc(loc - 4); | |
| break; | |
| case 0x40 ... 0x7f: | |
| // advance loc | |
| loc += (opcode[-1] & 0x3f) * code_alignment_factor; | |
| break; | |
| case 0xc0 ... 0xff: | |
| break; | |
| default: | |
| return EDYNSCS_INVALID_CFA_OPCODE; | |
| } | |
| } | |
| return 0; | |
| } | |
| int scs_patch(const u8 eh_frame[], int size, bool skip_dry_run) | |
| { | |
| int code_alignment_factor = 1; | |
| bool fde_use_sdata8 = false; | |
| const u8 *p = eh_frame; | |
| while (size > 4) { | |
| const struct eh_frame *frame = (const void *)p; | |
| int ret; | |
| if (frame->size == 0 || | |
| frame->size == U32_MAX || | |
| frame->size > size) | |
| break; | |
| if (frame->cie_id_or_pointer == 0) { | |
| /* | |
| * Require presence of augmentation data (z) with a | |
| * specifier for the size of the FDE initial_loc and | |
| * range fields (R), and nothing else. | |
| */ | |
| if (strcmp(frame->augmentation_string, "zR")) | |
| return EDYNSCS_INVALID_CIE_HEADER; | |
| /* | |
| * The code alignment factor is a uleb128 encoded field | |
| * but given that the only sensible values are 1 or 4, | |
| * there is no point in decoding the whole thing. Also | |
| * sanity check the size of the data alignment factor | |
| * field, and the values of the return address register | |
| * and augmentation data size fields. | |
| */ | |
| if ((frame->code_alignment_factor & BIT(7)) || | |
| (frame->data_alignment_factor & BIT(7)) || | |
| frame->return_address_register != 30 || | |
| frame->augmentation_data_size != 1) | |
| return EDYNSCS_INVALID_CIE_HEADER; | |
| code_alignment_factor = frame->code_alignment_factor; | |
| switch (frame->fde_pointer_format) { | |
| case DW_EH_PE_pcrel | DW_EH_PE_sdata4: | |
| fde_use_sdata8 = false; | |
| break; | |
| case DW_EH_PE_pcrel | DW_EH_PE_sdata8: | |
| fde_use_sdata8 = true; | |
| break; | |
| default: | |
| return EDYNSCS_INVALID_CIE_SDATA_SIZE; | |
| } | |
| } else { | |
| ret = scs_handle_fde_frame(frame, code_alignment_factor, | |
| fde_use_sdata8, !skip_dry_run); | |
| if (ret) | |
| return ret; | |
| if (!skip_dry_run) | |
| scs_handle_fde_frame(frame, code_alignment_factor, | |
| fde_use_sdata8, false); | |
| } | |
| p += sizeof(frame->size) + frame->size; | |
| size -= sizeof(frame->size) + frame->size; | |
| } | |
| return 0; | |
| } |