/*
 * zipl - zSeries Initial Program Loader tool
 *
 * Common functions for stand-alone dump tools
 *
 * Copyright IBM Corp. 2013, 2023
 *
 * 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 <stdarg.h>

#include "lib/zt_common.h"
#include "boot/error.h"
#include "dump/s390_dump.h"

#include "libc.h"
#include "sclp.h"
#include "stage2dump.h"
#include "boot/ipl.h"
#include "boot/os_info.h"
#include "boot/s390.h"

#define CPU_ADDRESS_MAX	1000
#define MACHINE_HAS_VX	machine_has_vx

/*
 * Static globals
 */
static uint8_t machine_has_vx;

/*
 * Tail parameters
 */
struct stage2dump_parm_tail parm_tail = {
	.mem_upper_limit = 0xffffffffffffffffULL,
};

/*
 * Globals
 */
struct df_s390_hdr *dump_hdr;
unsigned long total_dump_size;

/*
 * Init dumper: Allocate standard pages, set timers
 */
static void init_early(void)
{
	/* Allocate dump header */
	dump_hdr = (void *) get_zeroed_page();

	/* Set clock comparator and CPU timer to future */
	set_clock_comparator(0xffffffffffffffffULL);
	set_cpu_timer(0x7fffffffffffffffULL);
	if (test_facility(129))
		machine_has_vx = 1;
}

/*
 * Print progress message
 */
void progress_print(unsigned long addr)
{
	/* Print a message approximately every 5 sec */
	static unsigned long delta = 5 * (1UL << 32);
	static unsigned long next;
	unsigned long time = get_tod_clock();

	if (next == 0) {
		next = time + delta;
		return;
	}
	if (time < next && addr != dump_hdr->mem_size)
		return;
	printf("%08lu / %08lu MB (Dump file size %08lu MB)", addr >> 20,
	       dump_hdr->mem_size >> 20, total_dump_size >> 20);
	next = time + delta;
}

/*
 * Check if one MB memory after addr is zero
 */
static int __is_zero_mb(unsigned long addr)
{
	register unsigned long _addr1 asm("2") = (unsigned long) addr;
	register unsigned long _len1  asm("3") = (unsigned long) MIB;
	register unsigned long _addr2 asm("4") = (unsigned long) addr;
	register unsigned long _len2  asm("5") = (unsigned long) 0;
	unsigned int ipm;

	asm volatile(
		"0:     clcle   %1,%3,0(0)\n"
		"       jo      0b\n"
		"       ipm     %0\n"
		"       srl     %0,28\n"
		: "=d" (ipm), "+d" (_addr1), "+d" (_len1), "+d" (_addr2), "+d" (_len2)
		:
		: "cc");
	return !ipm;
}

/*
 * Check if the megabyte contains all zeroes.
 */
int is_zero_mb(unsigned long addr)
{
	if (!page_is_valid(addr))
		return 1;
	if (__is_zero_mb(addr))
		return 1;
	return 0;
}

/*
 * Find next non-zero megabyte in the address range
 */
static unsigned long find_next_non_zero(unsigned long start, unsigned long end)
{
	while (start < end) {
		if (!is_zero_mb(start))
			break;
		start += MIB;
	}
	if (start > end)
		start = end;
	return start;
}

/*
 * Find next zero megabyte in the address range
 */
static unsigned long find_next_zero(unsigned long start, unsigned long end)
{
	while (start < end) {
		if (is_zero_mb(start))
			break;
		start += MIB;
	}
	if (start > end)
		start = end;
	return start;
}

/*
 * Find first continuous set of non-zero megabytes in the specified
 * address range and update *dump_segm structure accordingly. The segment's
 * length is truncated to max_len if required. Afterwards, look for the next
 * non-zero megabyte. Return the address of the next non-zero megabyte or zero
 * if no more follow.
 */
unsigned long find_dump_segment(unsigned long start, unsigned long end,
				unsigned long max_len,
				struct df_s390_dump_segm_hdr *dump_segm)
{
	unsigned long addr, limit;

	memset(dump_segm, 0, sizeof(*dump_segm));
	/* Check the input values for MB alingment */
	if (!IS_ALIGNED(start, MIB) || !IS_ALIGNED(max_len, MIB))
		panic(EINTERNAL, "start or max_len not MB aligned");
	/* Search for dump segment start */
	addr = find_next_non_zero(start, end);
	if (addr == end)
		goto out;
	dump_segm->start = addr;
	/* Search for dump segment end */
	limit = end;
	if (max_len)
		limit = MIN(addr + max_len, end);
	addr = find_next_zero(dump_segm->start, limit);
	dump_segm->len = addr - dump_segm->start;
	/* Search for next dump segment start */
	addr = find_next_non_zero(addr, end);
	/* Return the start address of the next segment */
	if (addr < end)
		return addr;
out:
	/*
	 * Set the stop-marker indicating no more non-zero
	 * segments in the address range
	 */
	dump_segm->stop_marker = -1UL;
	return 0;
}

/*
 * Create IDA list starting at "addr" with "len" bytes
 */
void create_ida_list(unsigned long *list, int len, unsigned long addr,
		     unsigned long zero_page)
{
	unsigned long ida_addr;

	while (len > 0) {
		if (zero_page)
			ida_addr = page_is_valid(addr) ? addr : zero_page;
		else
			ida_addr = addr;
		*list = ida_addr;
		list++;
		addr += PAGE_SIZE;
		len -= PAGE_SIZE;
	}
}

/*
 * Initialize s390 dump header
 */
static void df_s390_dump_init(void)
{
	struct df_s390_hdr *dh = dump_hdr;

	dh->magic = DF_S390_MAGIC_EXT;
	dh->hdr_size = DF_S390_HDR_SIZE;
	dh->page_size = PAGE_SIZE;
	dh->dump_level = 4;
	dh->version = 2;
	dh->mem_start = 0;
	dh->arch = DF_S390_ARCH_64;
	dh->build_arch = DF_S390_ARCH_64;
	get_cpu_id((struct cpuid *) &dh->cpu_id);
	dh->tod = get_tod_clock();
	dh->volnr = 0;
	dh->zlib_version_s390 = 0;
	/*
	 * Ignore compression if --no-compress attribute has been specified
	 * or DFLTCC facility not available.
	 */
	if (test_facility(DFLTCC_FACILITY) &&
	    parm_tail.no_compress == 0) {
		dh->zlib_version_s390 = 1; /* Indicate interlnal Zlib version */
		dh->zlib_entry_size = DUMP_SEGM_ZLIB_ENTSIZE;
	}
}

/*
 * Initialize page with end marker
 */
void df_s390_em_page_init(unsigned long addr)
{
	struct df_s390_em *em = (void *) addr;

	em->magic = DF_S390_EM_MAGIC;
	em->tod = get_tod_clock();
}

/*
 * Find out memory size
 */
static void mem_and_cpu_init(void)
{
	unsigned long mem_size_max, mem_size, addr, rnmax, rzm;
	struct read_info_sccb *sccb;
	unsigned int mtid_prev;

	sccb = (void *) get_zeroed_page();
	/* Get memory max */
	if (sclp_read_info(sccb))
		panic(EMEMCOUNT, "Could not evaluate memory layout");
	rnmax = sccb->rnmax ? sccb->rnmax : sccb->rnmax2;
	rzm = sccb->rnsize ? sccb->rnsize : sccb->rnsize2;
	rzm <<= 20;
	mem_size_max = rnmax * rzm;
	mem_size = 0;
	/* Find out real memory end without standby memory */
	for (addr = 0; addr < mem_size_max; addr += rzm) {
		if (!page_is_valid(addr))
			continue;
		mem_size = addr + rzm;
	}
	/* Restore maximum thread ID of previous system */
	mtid_prev = (sccb->fac42 & 0x80) ? (sccb->fac66 & 31) : 0;
	sigp(0, SIGP_SET_MULTI_THREADING, mtid_prev, NULL);

	free_page((unsigned long) sccb);

	dump_hdr->mem_size_real = mem_size;
	/* Check if we have an upper limit */
	if (mem_size > parm_tail.mem_upper_limit) {
		printf("Using memory limit");
		mem_size = parm_tail.mem_upper_limit;
	}
	dump_hdr->mem_size = dump_hdr->mem_end = mem_size;
	dump_hdr->num_pages = dump_hdr->mem_size >> 12;
}

/*
 * Copy 64 bit lowcore after store status
 */
static void copy_lowcore_64(void)
{
	char *real_cpu_cnt_ptr = ((char *) &dump_hdr->mem_size_real) + 11;
	char *cpu_cnt_ptr = ((char *) &dump_hdr->mem_size_real) + 9;
	unsigned long prefix;
	uint16_t cpu_cnt = 0;

	prefix = S390_lowcore.prefixreg_save_area;

	/* Need memcpy because of aligment problem of members */
	memcpy(&cpu_cnt, real_cpu_cnt_ptr, sizeof(cpu_cnt));
	cpu_cnt++;
	memcpy(real_cpu_cnt_ptr, &cpu_cnt, sizeof(cpu_cnt));

	if (prefix < 0x10000) /* if < linux-start addr */
		return;
	if (prefix % 0x1000) /* check page alignment */
		return;

	/* Save lowcore pointer (32 bit) in dump header */
	memcpy(&cpu_cnt, cpu_cnt_ptr, sizeof(cpu_cnt));
	dump_hdr->lc_vec[cpu_cnt] = prefix;
	cpu_cnt++;
	memcpy(cpu_cnt_ptr, &cpu_cnt, sizeof(cpu_cnt));
	/*
	 *  |-----------------------------------------------------------|
	 *  | Decimal |  Length   | Data                                |
	 *  | Address |  in Bytes |                                     |
	 *  |_________|___________|_____________________________________|
	 *  | 163     | 1         | Architectural Mode ID               |
	 *  | 4608    | 128       | Fl-pt registers 0-15                |
	 *  | 4736    | 128       | General registers 0-15              |
	 *  | 4864    | 16        | Current PSW                         |
	 *  | 4888    | 4         | Prefix register                     |
	 *  | 4892    | 4         | Fl-pt control register              |
	 *  | 4900    | 4         | TOD programmable register           |
	 *  | 4904    | 8         | CPU timer                           |
	 *  | 4912    | 1         | Zeros                               |
	 *  | 4913    | 7         | Bits 0-55 of clock comparator       |
	 *  | 4928    | 64        | Access registers 0-15               |
	 *  | 4992    | 128       | Control registers 0-15              |
	 *  |_________|___________|_____________________________________|
	 */
	memcpy((void *) prefix + 4608, (void *) 4608, 272);
	memcpy((void *) prefix + 4888, (void *) 4888, 8);
	memcpy((void *) prefix + 4900, (void *) 4900, 20);
	memcpy((void *) prefix + 4928, (void *) 4928, 192);
}

/*
 * Has machine VX and has kernel prepared save area?
 */
static int should_dump_vx(struct _lowcore *lc)
{
	unsigned long vx_sa;

	if (!MACHINE_HAS_VX)
		return 0;
	vx_sa = lc->vector_save_area_addr;
	if (vx_sa == 0 || (vx_sa % 1024) != 0)
		return 0;
	if (!page_is_valid(vx_sa))
		return 0;
	return 1;
}

/*
 * Do store status
 */
static void store_status(void)
{
	unsigned short current_cpu;
	struct _lowcore *lc;
	unsigned long page;
	int addr, cc;

	/* Save absolute zero lowcore */
	page = get_zeroed_page();
	memcpy((void *) page, (void *) 0x1000, PAGE_SIZE);

	current_cpu = stap();

	lc = (void *)(unsigned long) S390_lowcore.prefixreg_save_area;
	if (should_dump_vx(lc))
		save_vx_regs_safe((__vector128 *) lc->vector_save_area_addr);
	copy_lowcore_64();
	for (addr = 0; addr < CPU_ADDRESS_MAX; addr++) {
		if (addr == current_cpu)
			continue;
		cc = sigp_busy(addr, SIGP_STOP_AND_STORE_STATUS, 0, NULL);
		if (cc != SIGP_CC_ORDER_CODE_ACCEPTED)
			continue;
		lc = (void *)(unsigned long) S390_lowcore.prefixreg_save_area;
		copy_lowcore_64();
		if (!should_dump_vx(lc))
			continue;
		sigp_busy(addr, SIGP_STORE_ASTATUS_AT_ADDRESS,
			  lc->vector_save_area_addr, NULL);
	}
	/* Restore absolute zero lowcore */
	memcpy((void *) 0x1000, (void *) page, PAGE_SIZE);
	free_page(page);
}

/*
 * Perform reipl: check lowcore for the address of an IPL Information
 * Block followed by a valid checksum (as defined in lowcore.h and set
 * by ipl.c). In case of match use diag308 to IPL.
 */
static __noreturn void dump_exit(unsigned long code)
{
	struct ipl_parameter_block *ipib = (struct ipl_parameter_block *)S390_lowcore.ipib;
	const struct os_info *os_info = (struct os_info *)S390_lowcore.os_info;
	const struct os_info_entry *os_info_entry = NULL;
	unsigned long os_info_flags = 0;
	uint32_t csum;

	if (!ipib)
		libc_stop(code);
	csum = csum_partial(ipib, ipib->hdr.len, 0);
	/* Verify ipib checksum against the value stored in lowcore */
	if (csum != S390_lowcore.ipib_checksum)
		libc_stop(code);
	diag308(DIAG308_SET, ipib);
	/*
	 * Check os_info flags entry for REIPL_CLEAR flag and make
	 * diag308 call with a proper subcode.
	 * Verify the bit flags field size before accessing it.
	 */
	if (os_info_check(os_info) == 0) {
		os_info_entry = &os_info->entry[OS_INFO_FLAGS_ENTRY];
		if (os_info_entry_is_valid(os_info_entry) &&
		    os_info_entry->size >= OS_INFO_FLAGS_ENTRY_SIZE)
			os_info_flags = *((unsigned long *)os_info_entry->addr);
		else
			printf("Warning: os_info flags entry is missing or corrupted");
	} else {
		printf("Warning: os_info is missing or corrupted");
	}
	if (os_info_flags & OS_INFO_FLAG_REIPL_CLEAR)
		diag308(DIAG308_LOAD_CLEAR, NULL);
	/* Use special diag308 subcode for CCW normal ipl */
	if (ipib->pb0_hdr.pbt == IPL_PBT_CCW)
		diag308(DIAG308_LOAD_NORMAL_DUMP, NULL);
	else
		diag308(DIAG308_LOAD_NORMAL, NULL);
	__builtin_unreachable();
}

/*
 * Print message and exit dumper
 */
void panic_notify(unsigned long code)
{
	printf("Dump failed");
	dump_exit(code);
}

/*
 * Create stand-alone dump
 */
void __noreturn start(void)
{
	init_early();
	dt_device_parm_setup();
	sclp_setup(SCLP_INIT);
	/* Store facility-list for future checks */
	stfle(S390_lowcore.stfle_fac_list,
	      ARRAY_SIZE(S390_lowcore.stfle_fac_list));
	dt_device_enable();
	df_s390_dump_init();
	printf("zIPL v%s dump tool (64 bit)", RELEASE_STRING);
	printf("Dumping 64 bit OS");
	if (parm_tail.no_compress)
		printf("Dump compression prevented by the --no-compress attribute");
	mem_and_cpu_init();
	store_status();
	dt_dump_mem();
	printf("Dump successful");
	dump_exit(0);
}
