/*
 * zipl - zSeries Initial Program Loader tool
 *
 * Main program for stage3 bootloader
 *
 * Copyright IBM Corp. 2013, 2018
 *
 * s390-tools is free software; you can redistribute it and/or modify
 * it under the terms of the MIT license. See LICENSE for details.
 */

#include "libc.h"
#include "sclp.h"
#include "menu.h"
#include "boot/error.h"
#include "boot/sigp.h"
#include "boot/s390.h"
#include "boot/sigp.h"
#include "boot/linux_layout.h"
#include "boot/loaders_layout.h"

#include "stage3.h"
#include "ebcdic.h"
#include "ebcdic_conv.h"

#define for_each_rb_entry(entry, rb) \
	for (entry = rb->entries; \
	     (void *) entry + sizeof(*entry) <= (void *) rb + rb->len; \
	     entry++)

static const char *msg_sipl_inval = "Secure boot failure: invalid load address";
static const char *msg_sipl_unverified = "Secure boot failure: unverified load address";
static const char *msg_sipl_noparm = "Secure boot failure: unable to load ipl parameter";

unsigned int
is_verified_address(unsigned long image_addr)
{
	struct ipl_rb_component_entry *comp;
	struct ipl_rb_components *comps;
	struct ipl_pl_hdr *pl_hdr;
	struct ipl_rl_hdr *rl_hdr;
	struct ipl_rb_hdr *rb_hdr;
	unsigned long tmp;
	void *rl_end;

	/*
	 * There is an IPL report, to find it load the pointer to the
	 * IPL parameter information block from lowcore and skip past
	 * the IPL parameter list, then align the address to a double
	 * word boundary.
	 */
	tmp = (unsigned long) S390_lowcore.ipl_parmblock_ptr;
	pl_hdr = (struct ipl_pl_hdr *) tmp;
	tmp = (tmp + pl_hdr->len + 7) & -8UL;
	rl_hdr = (struct ipl_rl_hdr *) tmp;
	/* Walk through the IPL report blocks in the IPL Report list */
	comps = NULL;
	rl_end = (void *) rl_hdr + rl_hdr->len;
	rb_hdr = (void *) rl_hdr + sizeof(*rl_hdr);
	while ((void *) rb_hdr + sizeof(*rb_hdr) < rl_end &&
	       (void *) rb_hdr + rb_hdr->len <= rl_end) {
		switch (rb_hdr->rbt) {
		case IPL_RBT_COMPONENTS:
			comps = (struct ipl_rb_components *) rb_hdr;
			break;
		default:
			break;
		}

		rb_hdr = (void *) rb_hdr + rb_hdr->len;
	}

	if (!comps)
		return 0;

	for_each_rb_entry(comp, comps) {
		if (image_addr == comp->addr &&
		    comp->flags & IPL_RB_COMPONENT_FLAG_SIGNED &&
		    comp->flags & IPL_RB_COMPONENT_FLAG_VERIFIED)
			return 1;
	}
	return 0;
}

unsigned int
secure_boot_enabled()
{
	struct ipl_pl_hdr *pl_hdr;
	unsigned int rc;

	pl_hdr = (void *)get_zeroed_page();
	switch (diag308(DIAG308_STORE, pl_hdr)) {
	case DIAG308_RC_OK:
		rc = pl_hdr->version <= IPL_MAX_SUPPORTED_VERSION &&
			!!(pl_hdr->flags & IPL_FLAG_SECURE);
		break;
	case DIAG308_RC_NOCONFIG:
		rc = 0;
		break;
	default:
		panic(ESECUREBOOT, "%s", msg_sipl_noparm);
		break;
	}
	free_page((unsigned long) pl_hdr);

	return rc;
}

#define ZIPL_ENVBLK_SIGNATURE	"# zIPL Environment Block\n"
#define ENVBLK_SIGNATURE_SIZE   (sizeof(ZIPL_ENVBLK_SIGNATURE) - 1)
#define STR_HASH_SIZE (16)

struct env_hash_entry {
	unsigned int next;
	unsigned int name;
};

/**
 * ITEMS: pointer to pre-allocated page, where the new item will be allocated
 * NAME: null-terminated name
 */
static void hash_table_add(struct env_hash_entry *items,
			   struct env_hash_entry **buckets,
			   unsigned int *new_idx,
			   unsigned long name)
{
	struct env_hash_entry *new;
	unsigned int hash;

	new = &items[*new_idx];
	new->name = name;
	hash = strhash((unsigned char *)name, NULL, STR_HASH_SIZE);
	new->next = (unsigned long)buckets[hash];
	buckets[hash] = new;
	(*new_idx)++;
}

/**
 * Check if string STR coincides with string SHORT_STR prepended
 * with a logical PREFIX (if any).
 * Return 1, if coincides. Otherwise, return 0.
 */
static int str_eq(char *str, char *short_str, char *prefix)
{
	if (prefix)
		return strlen(str) == strlen(short_str) + 1 &&
			*str == *prefix &&
			!strncmp(str + 1, short_str, strlen(short_str));
	return strlen(str) == strlen(short_str) &&
		!strncmp(str, short_str, strlen(str));
}

static struct env_hash_entry *hash_table_find(struct env_hash_entry **buckets,
					      char *name, char *prefix)
{
	struct env_hash_entry *item;

	if (strlen(name) == 0)
		return NULL;
	item = buckets[strhash((unsigned char *)name, prefix, STR_HASH_SIZE)];
	while (item) {
		if (str_eq((char *)(unsigned long)item->name, name, prefix))
			return item;
		item = (struct env_hash_entry *)(unsigned long)item->next;
	}
	return NULL;
}

/**
 * Format of environment block:
 *
 * magic'\n'name1=value1'\n'...nameN=valueN'\n'zero-padding
 */
static void parse_envblk(struct env_hash_entry *items,
			 struct env_hash_entry **buckets)
{
	char sgn[] = ZIPL_ENVBLK_SIGNATURE;
	unsigned int len = 0;
	unsigned int off = 0;
	unsigned int idx = 0;
	char *equation;
	char *newline;

	ebcdic_to_ascii((unsigned char *)sgn,
			(unsigned char *)sgn, ENVBLK_SIGNATURE_SIZE);

	if (strncmp((char *)_stage3_parms.envblk_addr, sgn,
		    ENVBLK_SIGNATURE_SIZE))
		goto bad_envblk;

	/* The first newline is located in the signature */
	newline = (char *)_stage3_parms.envblk_addr + ENVBLK_SIGNATURE_SIZE - 1;
	/*
	 * calculate significant length of the environment block
	 * (excluding trailing zeros)
	 */
	while (len < _stage3_parms.envblk_len - ENVBLK_SIGNATURE_SIZE) {
		if (newline[len + 1] == 0)
			break;
		len++;
	}
	while (off < len && idx < PAGE_SIZE / sizeof(struct env_hash_entry)) {
		equation = strchr(newline + 1, 0x3D /* = */);
		if (!equation)
			/* '=' not found */
			goto bad_envblk;
		/* null-terminate the name */
		*equation = 0;
		hash_table_add(items, buckets, &idx,
			       (unsigned long)(newline + 1));
		off += (equation - newline);
		newline = strchr(equation + 1, 0x0A /* /n */);
		if (!newline)
			/* '/n' not found */
			goto bad_envblk;
		/* null-terminate the value */
		*newline = 0;
		off += (newline - equation);
	}
	return;
bad_envblk:
	printf("Bad envblk\n");
}

/**
 * Lookup by NAME prepended with optional PREFIX in the hash table.
 * First, search by prefixed NAME. If nothing was found, then search
 * by NAME without prefix
 */
static struct env_hash_entry *lookup_item(struct env_hash_entry **buckets,
					  char *name, char *prefix)
{
	struct env_hash_entry *result;

	result = hash_table_find(buckets, name, prefix);
	if (result)
		return result;
	return prefix ? hash_table_find(buckets, name, NULL) : NULL;
}

/**
 * Find all instances of the pattern ${NAME} in the command line, and replace
 * each one with corresponding VALUE as found in the hash table by NAME
 * and SITE_ID, using lookup_item() search procedure.
 *
 * CMDL_LEN: length of the command line to be processed
 * BUCKETS: hash table of pairs (NAME, VALUE)
 * SITE_ID: pointer to an alphanumerical string representing a number
 */
void process_parm_line(struct env_hash_entry **buckets, unsigned int cmdl_len,
		       unsigned int max_len, char *site_id)
{
	struct env_hash_entry *item;
	char empty_str = 0;
	char *cmdl_end;
	char *start;
	char *end;
	char *val;
	int len;

	start = (char *)COMMAND_LINE;
	cmdl_end = start + cmdl_len;

	while (start < cmdl_end) {
		start = strchr(start, 0x24 /* $ */);

		if (start == NULL || start + 3 >= cmdl_end)
			/* no more instances */
			break;
		if (*(start + 1) != 0x7B /* { */) {
			/* skip "$" without braces */
			start += 1;
			continue;
		}
		end = strchr(start, 0x7D /* } */);
		if (end == NULL || end >= cmdl_end)
			/* no more instances */
			break;
		/*
		 * terminate the NAME and find a respective item
		 * in the hash table by that NAME prefixed with
		 * site-ID (if any)
		 */
		*end = 0;
		item = lookup_item(buckets, start + 2, site_id);
		if (!item) {
			/*
			 * Item to replace with not found.
			 * The case of "${}" (empty NAME) also gets here!
			 *
			 * Assign the empty string to remove the instance
			 * from the command line
			 */
			val = &empty_str;
		} else {
			val = (char *)(unsigned long)item->name +
				strlen((char *)(unsigned long)item->name) + 1;
		}
		/*
		 * try to replace the instance by VALUE as found
		 * in the hash table by NAME
		 */
		len = strlen(val);

		if ((cmdl_end - (char *)COMMAND_LINE) + len -
		    (end - start + 1) >= max_len)
			/* VALUE doesn't fit */
			break;
		/*
		 * make a room with the beginning at "$" by moving
		 * the rest of parm line at the right of "}" to @dst,
		 * which is (offset of "$" + size of the VALUE")
		 */
		memmove(start + len, end + 1, cmdl_end - (end + 1));
		/*
		 * copy VALUE to the room
		 */
		memcpy(start, val, len);
		cmdl_end += (len - (end - start + 1));
		start += len;
	}
	if (cmdl_len > cmdl_end - (char *)COMMAND_LINE)
		/*
		 * the resulted command line is shorter than the
		 * original one. Erase the garbage at the end
		 */
		memset(cmdl_end, 0,
		       cmdl_len - (cmdl_end - (char *)COMMAND_LINE));
}

/*
 * This represents a number as a pair consisting of an
 * alphanumerical string, and a variable which contains
 * its numerical value (as a result of strtoul conversion)
 */
struct nr_pair {
	char str[PARAM_SIZE];
	unsigned long val;
};

#define IPL_SC	  (*((struct subchannel_id *) &S390_lowcore.subchannel_id))

/**
 * Parse LOADPARM to identify a site to be activated.
 * If the site is identified, then store its ID in the structure
 * pointed out by NP
 */
static void get_active_site_by_loadparm(struct nr_pair *np)
{
	char loadparm[PARAM_SIZE + 1];
	char *endptr;
	int i = 0;

	memset(loadparm, 0, sizeof(loadparm));

	if (sclp_param(loadparm))
		return;
	/*
	 * extract site-ID as a sequence of digits after the first
	 * leading 'S'
	 */
	while (i < PARAM_SIZE) {
		if (loadparm[i++] == 'S') {
			if (loadparm[i] == 'S') {
				/*
				 * the case of automatic
				 * site activation by SSID
				 */
				if ((IPL_SC).one) {
					np->val = (IPL_SC).ssid;
					snprintf(np->str, PARAM_SIZE - 1,
						 "%u", np->val);
					return;
				}
				/* site undefined */
				return;
			}
			np->val = ebcdic_strtoul(&loadparm[i], &endptr, 10);
			memcpy(np->str, &loadparm[i], endptr - &loadparm[i]);
			/*
			 * not more than (PARAM_SIZE - 1) bytes were copied!
			 */
			return;
		}
	}
}

static char *get_active_site(struct nr_pair *np)
{
	memset(np->str, 0, sizeof(np->str));
	np->val = 0;

	get_active_site_by_loadparm(np);
	if (!np->str[0]) {
		/* No site-ID was found in LOADPARM */
		return NULL;
	}
	ebcdic_to_ascii((unsigned char *)np->str,
			(unsigned char *)np->str,
			strlen(np->str));
	return np->str;
}

/**
 * LEN: length of the command line to be processed
 */
static void handle_environment(unsigned int len, unsigned int max_len)
{
	struct env_hash_entry *buckets[STR_HASH_SIZE];
	struct env_hash_entry *items;
	struct nr_pair np;

	if (_stage3_parms.envblk_addr == 0 ||
	    _stage3_parms.envblk_addr == UNSPECIFIED_ADDRESS)
		return;

	memset(buckets, 0, sizeof(buckets));
	items = (struct env_hash_entry *)get_zeroed_page();
	/*
	 * scan in-memory environment block and populate hash table
	 */
	parse_envblk(items, buckets);
	/*
	 * find environment variables in the command line and
	 * replace them with their values as found in the hash table
	 */
	process_parm_line(buckets, len, max_len, get_active_site(&np));
	free_page((unsigned long)items);
}

static void verify_secure_boot(void)
{
	/*
	 * IPL process is secure we have to use default IPL values and
	 * check if the psw jump address is within at the start of a
	 * verified component. If it is not IPL is aborted.
	 */
	if (_stage3_parms.image_addr != IMAGE_LOAD_ADDRESS ||
	    _stage3_parms.load_psw != DEFAULT_PSW_LOAD)
		panic(ESECUREBOOT, "%s", msg_sipl_inval);

	if (!is_verified_address(_stage3_parms.load_psw & PSW32_ADDR_MASK))
		panic(ESECUREBOOT, "%s", msg_sipl_unverified);
}

static void setup_cmdline(void)
{
	char *cextra = (char *)COMMAND_LINE_EXTRA;
	char *cmdline =  (char *)COMMAND_LINE;
	unsigned int cmdline_len = 0;
	unsigned int max_cmdline_len = *(unsigned long *)MAX_COMMAND_LINE_SIZE;

	if (!max_cmdline_len)
		max_cmdline_len = LEGACY_COMMAND_LINE_SIZE;

	/* if valid command line is given, copy it into new kernel space */
	if (_stage3_parms.parm_addr != UNSPECIFIED_ADDRESS) {
		strlcpy(cmdline, (void *)(unsigned long *)_stage3_parms.parm_addr,
		       max_cmdline_len);
	}
	/* determine length of original parm line */
	cmdline_len = MIN((unsigned int)strlen(cmdline), max_cmdline_len - 1);

	/* convert extra parameter to ascii */
	if (!_stage3_parms.extra_parm || !*cextra)
		goto noextra;

	/* Handle extra kernel parameters specified in DASD boot menu. */
	ebcdic_to_ascii((unsigned char *)cextra, (unsigned char *)cextra, COMMAND_LINE_EXTRA_SIZE);

	while (isspace(*cextra))
		cextra++;

	/*
	 * if extra parm string starts with '=' replace original string,
	 * else append
	 */
	if (*cextra == 0x3d) {
		strlcpy(cmdline, cextra+1, max_cmdline_len);
	} else if (*cextra && cmdline_len + 1 <= max_cmdline_len - 1) {
		/* add blank */
		cmdline[cmdline_len++] = 0x20;
		strlcpy(cmdline + cmdline_len, cextra, max_cmdline_len - cmdline_len);
	}

noextra:
	handle_environment(strlen(cmdline), max_cmdline_len);
}

void __noreturn start(void)
{
	unsigned int subchannel_id;

	if (secure_boot_enabled())
		verify_secure_boot();

	ebcdic_update_table();

	/*
	 * cut the kernel header
	 */
	memmove((void *)_stage3_parms.image_addr,
		(void *)_stage3_parms.image_addr + IMAGE_LOAD_ADDRESS,
		_stage3_parms.image_len - IMAGE_LOAD_ADDRESS);

	/* store subchannel ID into low core and into new kernel space */
	subchannel_id = S390_lowcore.subchannel_id;
	*(unsigned int *)__LC_IPLDEV = subchannel_id;
	*(unsigned long long *)IPL_DEVICE = subchannel_id;

	setup_cmdline();

	/* copy initrd start address and size intop new kernle space */
	*(unsigned long long *)INITRD_START = _stage3_parms.initrd_addr;
	*(unsigned long long *)INITRD_SIZE = _stage3_parms.initrd_len;

	/*
	 * store address of new kernel to 0 to be able to start it.
	 * -fno-delete-null-pointer-checks allows us to dereference 0 here,
	 * which otherwise would be an UB.
	 */
	*(volatile unsigned long long *)0 = _stage3_parms.load_psw;

	kdump_stage3();

	/* start new kernel */
	diag308(DIAG308_LOAD_NORMAL_RESET, NULL);
	__builtin_unreachable();
}

void panic_notify(unsigned long UNUSED(rc))
{
}
