Contributors: 3
Author Tokens Token Proportion Commits Commit Proportion
Ard Biesheuvel 897 99.34% 8 80.00%
Catalin Marinas 5 0.55% 1 10.00%
Thomas Gleixner 1 0.11% 1 10.00%
Total 903 10

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
// 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_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)
{
	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, true);
			if (ret)
				return ret;
			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;
}